Welcome to CSharp Labs

Type Switching with ObjectSwitch and ActionTypeSwitch

Thursday, May 30, 2013

Attempting to use the switch statement with non-primitive type or with non-constant case labels is a lesson in futility. The compiler cannot resolve type definitions before building, resulting in a compiler error:

To compromise with the compiler, I have created the ObjectSwitch and ActionTypeSwitch classes which have similar functionality as the switch statement by means of artificial switching on any type of class.

How it Works

The abstract ObjectSwitch class maintains a dictionary of case labels and their respective delegates and requires two type arguments: TSwitch which represents the type of object to compare and TDelegate, a type of delegate to be raised (and validated in the static initializer):

    /// <summary>
    /// The ObjectSwitch class allows an object to invoke methods.
    /// </summary>
    /// <typeparam name="TSwitch">The type of object to compare.</typeparam>
    /// <typeparam name="TDelegate">The type of delegate to box or unbox.</typeparam>
    public abstract class ObjectSwitch<TSwitch, TDelegate> where TDelegate : class, ICloneable, ISerializable
    {
        /// <summary>
        /// The static initializer provides a way to restrict TDelegate to delegate types.
        /// </summary>
        static ObjectSwitch()
        {
            if (!typeof(TDelegate).IsSubclassOf(typeof(Delegate)))
                throw new InvalidOperationException(typeof(TDelegate).Name + " is not a delegate type.");
        }

        /// <summary>
        /// Defines a dictionary of cases and actions.
        /// </summary>
        private Dictionary<TSwitch, TDelegate> _TypeRefs;

        /// <summary>
        /// Initializes the TypeSwitch class.
        /// </summary>
        protected ObjectSwitch()
        {
            _TypeRefs = new Dictionary<TSwitch, TDelegate>();
        }

        ...
    }

When ObjectSwitch is subclassed, two abstract methods must be implemented to box and unbox the delegate since .NET 4.5 does not support delegate type constraints. The ActionTypeSwitch shows how to inherit ObjectSwitch for Action<Type> delegates:

    /// <summary>
    /// The ActionTypeSwitch class allows a type to invoke methods.
    /// </summary>
    public class ActionTypeSwitch : ObjectSwitch<Type, Action<Type>>
    {
        /// <summary>
        /// Invokes a method by looking up the associated case from the specified type.
        /// </summary>
        /// <param name="type">The type to search for.</param>
        public void Switch(Type type)
        {
            //gets associated delegate or default
            Action<Type> del = LookupDelegate(type);

            if (del != null)
                del(type);
        }

        /// <summary>
        /// Boxes the delegate.
        /// </summary>
        /// <param name="del">The delegate to box.</param>
        /// <returns>The boxed delegate.</returns>
        protected override Delegate BoxDelegate(Action<Type> del)
        {
            return del;
        }

        /// <summary>
        /// Unboxes the delegate.
        /// </summary>
        /// <param name="del">The delegate to unbox.</param>
        /// <returns>The unboxed delegate.</returns>
        protected override Action<Type> UnboxDelegate(Delegate del)
        {
            return (Action<Type>)del;
        }
    }

ObjectSwitch maintains an invocation list in the dictionary by using Delegate.Add and Delegate.Remove to chain together delegates when ObjectSwitch.Add or ObjectSwitch.Remove is called:

        /// <summary>
        /// Adds a key and associates it with the specified delegate.
        /// </summary>
        /// <param name="newKey">The key to associate with a delegate.</param>
        /// <param name="value">The delegate to invoke.</param>
        /// <exception cref="ArgumentNullException">newKey is null.</exception>
        /// <exception cref="ArgumentNullException">value is null.</exception>
        public void Add(TSwitch newKey, TDelegate value)
        {
            if (newKey == null)
                throw new ArgumentNullException("newKey");
            if (value == null)
                throw new ArgumentNullException("value");

            TDelegate current_value;
            if (_TypeRefs.TryGetValue(newKey, out current_value)) //if the key exists
                _TypeRefs[newKey] = UnboxDelegate(Delegate.Combine(BoxDelegate(current_value), BoxDelegate(value))); //combine delegates
            else //otherwise, add the key and delegate
                _TypeRefs[newKey] = value;
        }

        /// <summary>
        /// Removes a key and delegate.
        /// </summary>
        /// <param name="remKey">The key to remove.</param>
        /// <param name="value">The delegate to remove.</param>
        /// <exception cref="ArgumentNullException">remKey is null.</exception>
        /// <exception cref="ArgumentNullException">value is null.</exception>
        public void Remove(TSwitch remKey, TDelegate value)
        {
            if (remKey == null)
                throw new ArgumentNullException("remKey");

            if (value == null)
                throw new ArgumentNullException("value");

            TDelegate current_value;
            if (_TypeRefs.TryGetValue(remKey, out current_value)) //try to get the delegate
            {
                Delegate boxed_value = BoxDelegate(value); //boxes value to a delegate
                Delegate boxed_current_value = BoxDelegate(current_value); //boxes current value to a delegate
                Delegate[] invocations = boxed_current_value.GetInvocationList(); //get current invocation list

                if (invocations.Length - 1 == 0) //if there is only 1 delegate
                {
                    if (invocations[0] == boxed_current_value) //check if value
                        _TypeRefs.Remove(remKey); //drop from references
                }
                else //otherwise, remove the delegate
                    _TypeRefs[remKey] = UnboxDelegate(Delegate.Remove(boxed_current_value, boxed_value));
            }
        }

When ActionTypeSwitch.Switch is called with a specified type, ObjectSwitch.LookupDelegate retrieves the corresponding delegate or Default to be invoked:

        /// <summary>
        /// Gets or sets the default function to invoke.
        /// </summary>
        public TDelegate Default { get; set; }

        /// <summary>
        /// Looks up the specified key and returns an associated delegate.
        /// </summary>
        /// <param name="key">The key to look up the corresponding delegate from.</param>
        /// <returns>If the key exists, returns the corresponding delegate; if <see cref="Default"/> is not null, returns <see cref="Default"/>; otherwise, returns null.</returns>
        /// <exception cref="ArgumentNullException">key is null.</exception>
        protected TDelegate LookupDelegate(TSwitch key)
        {
            if (key == null)
                throw new ArgumentNullException("key");

            TDelegate value;
            if (_TypeRefs.TryGetValue(key, out value)) //try to get the value
                return value; //return the current value

            //otherwise return the default
            return Default;
        }

While the regular switch statement will outperform the ObjectSwitch class, look-up speed is very fast, close to O(1), while using the dictionary class to reference cases.

Using

To minimize the initialization performance hit, instantiate and reuse a single ActionTypeSwitch instance:

        /// <summary>
        /// Represents an object that allows a type to invoke a method.
        /// </summary>
        private ActionTypeSwitch _Controller;

        /// <summary>
        /// Ensures the _Controller field is initialized;
        /// </summary>
        private void EnsureControllerInitialized()
        {
            if (_Controller == null)
            {
                _Controller = new ActionTypeSwitch(); //initialize

                //add cases:
                _Controller.Add(typeof(string), (type) =>
                {
                    Console.WriteLine("String Case");
                });

                _Controller.Add(typeof(int), (type) =>
                {
                    Console.WriteLine("Integer Case");
                });

                _Controller.Add(typeof(bool), (type) =>
                {
                    Console.WriteLine("1st Boolean Case");
                });

                _Controller.Add(typeof(bool), (type) =>
                {
                    Console.WriteLine("2nd Boolean Case");
                });

                _Controller.Default = (type) =>
                {
                    Console.WriteLine("Default Case");
                };
            }
        }

        /// <summary>
        /// Switches on a type.
        /// </summary>
        public void ExampleTypeSwitching()
        {
            //ensure the ActionTypeSwitch instance has been initialized:
            EnsureControllerInitialized();

            //switches on types and prints results
            _Controller.Switch(typeof(string)); //String Case
            _Controller.Switch(typeof(int)); //Integer Case
            _Controller.Switch(typeof(bool)); //1st Boolean Case    2nd Boolean Case
            _Controller.Switch(typeof(byte)); //Default Case
        }

Download ObjectSwitch and ActionTypeSwitch Classes

Comments