Pattern Matching
To start I want to present the use of pattern matching in switch statements to work with different types, as follows:
public static double ComputeAreaModernSwitch(object shape)
{
switch (shape)
{
case Square s:
return s.Side * s.Side;
case Circle c:
return c.Radius * c.Radius * Math.PI;
case Rectangle r:
return r.Height * r.Length;
default:
throw new ArgumentException(
message: "shape is not a recognized shape",
paramName: nameof(shape));
}
}
Example taken from Pattern Matching - C# Guide.
Type Dictionary
With that out of the way, yes, you can write a dictionary... the trouble will be on the type of the items.
We can do this:
Dictionary<Type, Action<object>> dictionary;
// (initialize and populate somewhere else) ...
if (dictionary.TryGetValue(element.GetType(), out var action))
{
action(element);
}
However, here you have to use Action<object>
because we need to give a type to the items (and no, we can't say Action<?>
- well, we can do Action<dynamic>
but you cannot cast Action<someType>
to Action<dynamic>
), forcing you to cast inside the called method.
We can argue that a cast is a way to tell the compiler that we know something it does not. In this case that we know that that object is actually of a given type.
We could do a bit better/worse, depending on how you look at it...
Dictionary<Type, Delegate> dictionary;
// (initialize and populate somewhere else) ...
if (dictionary.TryGetValue(element.GetType(), out var @delegate))
{
@delegate.DynamicInvoke(element);
}
This is effectively late binding. We do not know the types at compile time... as developer you must ensure you provide a delegate of the correct type. However, if we are already enforcing knowledge that the compiler is unaware of, then this could be acceptable.
We can make a helper method to make it easier:
void SetMethod<T>(Action<T> action)
{
dictionary[typeof(T)] = action;
}
Here the compiler can check the type for the method is correct. Yet, from the point of view of the compiler this information is lost (not available) when you consume the dictionary. It is a kind of type erasure if you will.
Dynamic
Now, if we are forgoing types, we could use dynamic
following good answer by TheGeneral.
Addendum: Calling a known method (with MethodInfo)
You can call a method by its name, for example, if you have the following:
class Helper
{
public static void Method(T input)
{
Console.WriteLine(input.GetType());
}
}
You can do this:
var methodInfo = typeof(Helper).GetMethod("Method");
// ...
methodInfo.Invoke(null, new object[]{element});
You could then put all your methods in a helper class, and find them by the name (which you could derive from the name of the type).
If you want to call a known method that has a generic parameter, you can use MethodInfo. We need to be aware of whatever or not the method is static, and whatever or not the generic argument is part of the method definition or the declaring type definition...
On one hand, if you have something like this:
class Helper<T>
{
public static void Method(T input)
{
Console.WriteLine(input.GetType());
}
}
You can do this:
var helperType = typeof(Helper<>);
// ...
var specificMethodInfo = helperType.MakeGenericType(element.GetType()).GetMethod("Method");
specificMethodInfo.Invoke(null, new object[]{element});
On the other hand, if you have this:
class Helper
{
public static void Method<T>(T input)
{
Console.WriteLine(input.GetType());
}
}
You can do this:
var methodInfo = typeof(Helper).GetMethod("Method");
// ...
var specificMethodInfo = methodInfo.MakeGenericMethod(element.GetType());
specificMethodInfo.Invoke(null, new object[]{element});
Note: I pass null
as first parameter to invoke. That is the instance on which I am calling the method. None, because they are static. If they aren't then you need an instance... you could try creating one with Activator.CreateInstance
, for example.
Addendum: Finding what to call (Type Discovery)
Perhaps you have disparate method to call (they are not the same but with different generic argument), but you do not want to have the trouble of populate the dictionary by hand.
That is where Type Discovery comes in.
To begin with, I suggest to use an attribute, for example:
[AttributeUsage(AttributeTargets.Method)]
public sealed class DataHandlerAttribute : Attribute { }
Then we need a list of the types where we will search. If we will search on a known assembly we could do this:
var assembly = typeof(KnownType).GetTypeInfo().Assembly;
var types = assembly.GetTypes();
Note: if your target platform does not support this (.NET Standard 1.0 to 1.4), you will have to hand code the list of types.
Next, we need a predicate to check if a given type is one of the ones in which we are interested:
bool IsDataHandlerMethod(MethodInfo methodInfo)
{
var dataHandlerAttributes = return (DataHandlerAttribute[])item.GetCustomAttributes(typeof(DataHandlerAttribute), true);
if (attributes == null || attributes.Length == 0)
{
return false;
}
if (methodInfo.DeclaringType != null)
{
return false;
}
if (methodInfo.ReturnTpye != typeof(void))
{
return false;
}
var parameters = methodInfo.GetParameters();
if (parameters.Length != 1)
{
return false;
}
if (paramters[0].IsByRef || paramters[0].IsOut)
{
return false;
}
return true;
}
And a method to convert them into delegates:
(Type, Delegate) GetTypeDelegatePair(MethodInfo methodInfo)
{
var parameters = methodInfo.GetParameters();
var parameterType = parameters[0].ParameterType;
var parameterTypeArray = new []{parameterType};
var delegateType = typeof(Action<>).MakeGenericType(parameterTypeArray);
var target = null;
if (!methodInfo.IsStatic)
{
var declaringType = methodInfo.DeclaringType;
target = instance = Activator.CreateInstance(declaringType);
}
return (parameterType, methodInfo.CreateDelegate(delegateType, target));
}
And now we can do this:
var dataHandlers = types
.SelectMany(t => t.GetTypeInfo().GetMethods())
.Where(IsDataHandlerMethod)
.Select(GetTypeDelegatePair);
And we will have an enumerable of pairs of types and delegate that we can use to populate our dictionary.
Note: the above code still needs some work (for example, could we just call GetParameters
once?), and presumes a modern .NET target (extra work is needed to make it work in older platforms). Also notice the code for Type Discovery I present does not handle generic methods, you can check Type.IsGenericTypeDefinition
and MethodInfo.IsGenericMethodDefinition
... however, I would suggest to avoid them. In fact, it should be easy to modify for the case where you want to put all the methods in a single static class. You may also use a similar approach to get factory methods, for example.