简体   繁体   中英

How to call generic method of derived class from base class reference in C#

I have the following base class (omitted version):

class BaseClass
{  
}

I create the following derived classes:

class DataPreparationClass<T> : BaseClass
{
}

class DataClass<T,U> : DataPreparationClass<T>
{
  public virtual void Start<U>(U arg)
  {}
}

class DataClassMain : DataClass<InputData,Data>
{
  public override void Start(Data argument)
  {
    base.Start(argument);
  }
}

class DataClassAux : DataClass<InputData,AuxData>
{
  public override void Start(AuxData argument)
  {
    base.Start(argument);
  }
}

I have a List<BaseClass> containing various derived instances (there are more derived types) and I intend to call their Start method respectively:

List<BaseClass> instances = GetAllInstance();

foreach(BaseClass instance in instances)
{
  object arg = GetArgsForInstance(instance);
  // instance.Start(arg); //can't call this
}

However, as their common base is the BaseClass , I can't call Start without casting to...basicly every possible type as their types are unknown at the processing.

If I use dynamic :

 ((dynamic)target).Start(new Data("starting")); //target is of type DataClassMain<InputData,Data>

I get an exception:

Microsoft.CSharp.RuntimeBinder.RuntimeBinderException: 'The best overloaded method match for 'Client.DataClass<InputData,Data>.Start(Data)' has some invalid arguments'

So how should I call the unknown method?

I would say, your questions shows multiple flaws in your model:

  • by definition of your classes, there is no polymorphism inbetween you Start() methods: Start(Data) do not override Start<U>(U)
  • by definition of your GetArgsForInstance() method, you have lost the type information you need.
  • I would add that Classes that are called Classes and Data that are called Data and that are parameterized with their content are way too generic.

That saying, your question implies that you are not wanting to fix those flaws, maybe they are out of your control, so you have to live with it:

  • instead of not loosing the Type information, you ask for a way to retrieve it.
  • instead of using polymorphism to retrieve the type from the best place to retrieve do so (in my opinion), which is the Start method itself, you ask for a way to retrieve it in the calling code.

So, what I would try to do is:

  1. rework your GetArgsForInstance() method to be able not to loose this information, for instance, replace it by an object, something like:
        class DataClassMain : DataClass<InputData,Data>
        {
          public override void Start(ArgumentProvider argumentProvider)
          {
            Data argument = argumentProvider.getArgumentAsData(argumentProvider);
            base.Start(argument);
          }
        }
  1. if not possible, retrieve the types from the inside of the derived classes, for instance something like:
        public class DataClassMain : DataClass<InputData,Data>
        {
            public override void Start(object arg)
            {
                base.Start(arg);
                Data argAsData = (Data) arg;
            }
        }
  1. if not possible, that means you already have a set of constraint that is making your code hard to maintain, so let's go for a messy reflective thing, but you have to be aware that there is no polymorphism involved and get rid of your 'override' and 'virtual' modifier on Start() methods. Here is a fully working program, which output is:
DataClassMain
DataClassAux
        public static void Main(string[] args)
        {
            List<BaseClass> instances = GetAllInstance();

            foreach(BaseClass instance in instances)
            {
                object value = GetArgsForInstance(instance);
                messyInvoke(instance, value);
            }
        }

        private static void messyInvoke(BaseClass instance, object value)
        {
            MethodInfo method = instance.GetType().GetMethod("Start");
            if (method != null)
            {
                ParameterInfo[] parametersInfos = method.GetParameters();
                if (parametersInfos.Length == 1)
                {
                    object[] paramArray = {value};
                    method.Invoke(instance, paramArray);
                }
            }
        }

        public class BaseClass{
            public virtual Type GetTypeOfArgs()
            {
                return typeof(Toto);
            }
        }
        public class DataPreparationClass<T> : BaseClass {}

        public abstract class DataClass<T> : DataPreparationClass<T>
        {
        }
        public class DataClassMain : DataClass<Toto>
        {
            public void Start(Data arg)
            {
                Console.WriteLine("DataClassMain");
            }
        }

        public class DataClassAux : DataClass<Toto>
        {
            public void Start(AuxData argument)
            {
                Console.WriteLine("DataClassAux");
            }
        }

        private static object GetArgsForInstance(BaseClass isntance)
        {
            if (isntance is DataClassMain)
                return new Data();
            if (isntance is DataClassAux)
                return new AuxData();
            throw new ArgumentException();
        }

        private static List<BaseClass> GetAllInstance()
        {
            return new List<BaseClass> {new DataClassMain(), new DataClassAux()};
        }

        
        public class Toto{}
        
        public class DataClassInputData
        {
        }
        public class Data : DataClassInputData
        {
        }
        public class AuxData : DataClassInputData
        {
        }

So, the most straight forward answer to your question would be to use pattern matching to call the start method.

List<BaseClass> instances = GetAllInstance();
foreach(BaseClass instance in instances)
{
    object arg = GetArgsForInstance(instance);
    switch(instance){
        case DataClassMain d : d.Start((Data)arg); break;
        case DataClassAux a : a.Start((AuxData)arg);break;
        default: throw new Exception();
    }
}

But I do get the feeling this is an convoluted and inappropriate inheritance chain, and you should really consider using a factory and/or strategy pattern instead.

It's assumed that GetArgsForInstance allways will return the correct type with respect to the type it receives as an argument, and that the return types (Data, AuxData and so on) share a common base type. So we could do the type resolution directly with a signature of T GetArgsForInstance<T>(BaseClass b) . That way you can make sure you get args of the right type before you return it.

Since the Start overrides just pass along the call generic types, so the overrides in DataClassMain and DataClassAux are unnecessary.

If we modify DataClass a bit we can then do it like this:

class DataClass<T,U> : DataPreparationClass<T>
{
  public virtual void Start(U arg)
  {
      //Do somethin with arg
      
  }
  public void Call(Func<BaseClass,U> f){
     U data = f.Invoke(this);
     Start(data);
  }
}

and invoke it with

List<BaseClass> instances = GetAllInstance();

foreach(BaseClass instance in instances)
{
    switch(instance)
    {
        case DataClassMain d : d.Call(GetArgsForInstance<Data>); break;
        case DataClassAux a : a.Call(GetArgsForInstance<AuxData>);break;
        default: throw new Exception();
    }
}

The reason this is preferable is that we can let the compiler ensure that we only pass the appropriate types to the different methods, no casting needed.

But again, such a convoluted inheritance chain should almost always be avoided.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM