Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Compiler gives implicit conversion error || My generic method's constraint is an abstract generic class

I'm writing a program and found some common behavior, so I thought this would be an appropriate use case for an abstract base class.

This is a simplified version of my Abstract Base Class:

    public abstract class BehaviorClass<T> where T: IDomainObj
    {
      protected BehaviorClass(var x){...}
      public abstract void Create(List<T> list, out string message);
      public abstract void Delete(List<T> list, out string message);
      ...
    }

This is a simplified version of my Derived Class:

    public class DbSheets : BehaviorClass<Sheet>
    {
      public override void Create(List<Sheet> sheets, out string message){...}
      public override void Delete(List<Sheet> sheets, out string message){...}
      ...
    }

This is the Generic Method that I want to operate on my derived classes:

    public void Import<DbObj>() where DbObj : BehaviorClass<IDomainObj>
    {
      var instance = (DbObj)Activator.CreateInstance(typeof(DbObj), DbAccessor);

      // STEP 1: Remove existing values
      var existingValues = instance.Read();
      instance.Delete(existingValues, out message);

      // STEP 2: Create new IDomainObjects 
      var domainObjects = //LINQ Query.ToList();

      // STEP 3: Add new IDomainObjects to the instance
      instance.Create(domainObjects, message);
    }

Up to this point, everything compiles fine until I try to Call the Import Method.

    internal class Program
    {
      ...
      intermediary.Import<DbSheets>();
      ...
    }

This is the Resulting Error from trying to invoke the Import method

The type 'namespace.DbSheets' cannot be used as type parameter 'DbObj' in the generic type or method 'intermediary.Import<DbObj>()'. There is no implicit reference conversion from 'namespace.DbSheets' to 'namespace.BehaviorClass<IDomainObj>'.

Summary of my thought process: In essence, I want a generic method to operate only on the classes derived from BehaviorClass, since I can reliably know they share a set of common methods and properties. Resharper says if I remove the generic constraint on the Import method, that the code will compile. I'd rather not remove that constraint, since this method specifically relies on the fact that this shared behavior will exist.

Note: I'm using the IDomainObj interface as a way to limit the generic parameter to a specific set of classes. It doesn't contain any specific functionality at this point.

like image 637
Trevor.Screws Avatar asked Jul 24 '15 19:07

Trevor.Screws


2 Answers

It seems to me that you need two generic type parameters:

public void Import<TBehavior, TDomain>()
    where TBehavior : BehaviorClass<TDomain>
    where TDomain : IDomainObj
{
    var instance = (TBehavior) Activator.CreateInstance(typeof(TBehavior), DbAccessor);

    // STEP 1: Remove existing values
    var existingValues = instance.Read();
    instance.Delete(existingValues, out message);

    // STEP 2: Create new IDomainObjects 
    var domainObjects = //LINQ Query.ToList();

    // STEP 3: Add new IDomainObjects to the instance
    instance.Create(domainObjects, message);
}

Now you should be able to call:

Import<DbSheets, Sheet>();

The problem before is that a DbSheets isn't a BehaviorClass<IDomainObj> - you can't call sheets.Create(new List<IDomainObj>()) for example.

It's slightly clunky having to specify two type arguments, and there are probably ways to avoid it, but this is the simplest approach to start with, I think.

like image 100
Jon Skeet Avatar answered Nov 12 '22 21:11

Jon Skeet


Since the Import function appears to be tightly coupled to the Behavior class (almost as if it should be encapsulated within the Behavior class) why not do this:

public abstract class BehaviorClass<TBehavior, TDomainObj>
    where TBehavior : BehaviorClass<TBehavior, TDomainObj> 
    where TDomainObj : IDomainObj
{
  protected BehaviorClass(var x){...}
  public abstract void Create(List<T> list, out string message);
  public abstract void Delete(List<T> list, out string message);
  ...
  public static void Import()
  {
    var instance = (TBehavior)Activator.CreateInstance(typeof(TBehavior), DbAccessor); // <- where did DbAccessor come from?

    // STEP 1: Remove existing values
    var existingValues = instance.Read();
    instance.Delete(existingValues, out message);

    // STEP 2: Create new IDomainObjects 
    var domainObjects = //LINQ Query.ToList();

    // STEP 3: Add new IDomainObjects to the instance
    instance.Create(domainObjects, message);
  }
}

Use it like this:

public class DbSheets : BehaviorClass<DbSheets, Sheet>
{
  public override void Create(List<Sheet> sheets, out string message){...}
  public override void Delete(List<Sheet> sheets, out string message){...}
  ...
}

internal class Program
{
  ...
  DbSheets.Import();
  ...
}

Bonus:

Since you are hardcoding DbAccessor anyway (where does this come from), do the following to avoid Activator.CreateInstance in your code (it may still be used by the underlying framework, but that's not your concern and the framework team may optimize it later).

public abstract class BehaviorClass<TBehavior, TDomainObj>
    where TBehavior : BehaviorClass<TBehavior, TDomainObj>, new() 
    where TDomainObj : IDomainObj
{
  protected BehaviorClass():this(DbAccessor){} // <- where did DbAccessor come from originally?
  protected BehaviorClass(var x){...}
  public abstract void Create(List<T> list, out string message);
  public abstract void Delete(List<T> list, out string message);
  ...
  public static void Import()
  {
    var instance = new TBehavior();

    // STEP 1: Remove existing values
    var existingValues = instance.Read();
    instance.Delete(existingValues, out message);

    // STEP 2: Create new IDomainObjects 
    var domainObjects = //LINQ Query.ToList();

    // STEP 3: Add new IDomainObjects to the instance
    instance.Create(domainObjects, message);
  }
}
like image 21
Tyree Jackson Avatar answered Nov 12 '22 22:11

Tyree Jackson