简体   繁体   中英

C# use constraint generics to allow function only on parent types

I am trying to use generic constraints to only allow a generic function to be called on the parent types of another type.

Example:

public class SomeClass<Derived> 
    where Derived : class
{
    public void call<Parent>()
        where Parent : class
    {
        ParenthoodChecker<Derived, Parent> checker =
            new ParenthoodChecker<Derived, Parent>();
    }
}

public class ParenthoodChecker<Derived, Parent> 
    where Parent  : class 
    where Derived : Parent
{
    public ParenthoodChecker()
    {
    }
}

Currently, I'm getting the following error message:

Error CS0311: The type 'Derived' cannot be used as type parameter 'Derived' in the generic type or method 'ParenthoodChecker'. There is no implicit reference conversion from 'Derived' to 'Parent'. (CS0311)

Is there some way to enforce such a thing compile time? I'd prefer not checking during runtime and I feel like the compiler should be able to deduce this.

The problem you are facing is that the generic constraint (the where keyword) is only associated with the generic arguments of the class/method in which it is used. Therefore, when writing the generic method call<Parent> , constraints can only be defined on the argument Parent .

You could work around the problem by adding a new artificial generic argument to the method - that would complicate the signature but make it eventually correct from syntactical point of view:

public class SomeClass<Derived>
    where Derived : class
{
    public void call<Parent, NewDerived>()
        where Parent : class
        where NewDerived: Derived, Parent
    {
        ParenthoodChecker<NewDerived, Parent> checker =
            new ParenthoodChecker<NewDerived, Parent>();
    }
}

I presume that, other from being ugly, and apart from adding complexity, this solution would not incur incorrect behavior. The NewDerived type is still Derived .

On a higher theoretical ground, this is the problem of comparing values: if A > B and A > C , can we tell that B > C ? - Obviously not because the precise relationship between B and C is not described here.

Conversely, if you tell that Parent > NewDerived and Derived > NewDerived , that will be fine. But still you'll be lacking the proof that Parent > Derived . That is the whole reason why it is impossible (I think) to write such function which would let the compiler figure that Parent is really a supertype of Derived .

With the implementation given above, you are even free to call the method with the Derived in place of NewDerived :

class A { }
class B : A { }

SomeClass<B> s = new SomeClass<B>();
s.call<A, B>();

In this example, there are only two classes, A and B , so there is even no any other class to play the role of the fictitious NewDerived . The whole operation remains between types A (as the base) and B (as the derived).

Considering ParenthoodChecker in my opinion such check at compile time could be done in a simpler way:

public class Parent { }
public class Derived : Parent { }
public class NotDerived { }

Parent p1 = (Derived)null; // This will compile
Parent p2 = (NotDerived)null; // This won't compile

Most answers give a partial answer so I'll compile all the answers and reasons why they may not work for specific cases here.

Option 1 : reversing caller & callee (m.rogalski's comment)

public class SomeClass<Parent> //instead of derived
    where Parent : class
{
    public void call<Derived>()
        where Derived : class, Parent
    {
        ParentChecker<Derived, Parent> checker =  new ParentChecker<Derived, Parent>();
    }
}

This is the closest to a complete compile time check as you can get of course but it might not be feasible depending on the system design (which was my case).

Option 2 : using intermediate template type (Zoran Horvat's Answer)

public class SomeClass<Derived>
    where Derived : class
{
    public void call<Parent, NewDerived>()
        where Parent : class
        where NewDerived: Derived, Parent
    {
        ParentChecker<NewDerived, Parent> checker =
        new ParentChecker<NewDerived, Parent>();
    }
}

This works so long as you use it with correct input (which will be most cases) but the reason to use compile time checking is mostly to avoid having miss use of something so I don't feel it's a complete answer. The usability is weaker than ideal (you need to specify both types at the moment of the call) and it has potential counter examples such as :

public interface GrandP_A {}
public interface GrandP_B {}
public class Parent : GrandP_A {}
public class Child : Parent, GrandP_B {} 

SomeClass<Parent> instance = new SomeClass<Parent>();
instance.call<GrandP_B, Child>();//this compiles

The goal of SomeClass is to check, that SomeClass's generic type parameter is derived from the first generic type of the call function. In this case GrandP_B. However, GrandP_B is not a parent class to Parent but the call still compiles. So again, this only works when you use it well (passing Parent as the second generic type of .call)

Option 3 : Runtime checking

I had to compromise and use Runtime Checking instead. This is obviously not the way to go as a compile time solution but it's the only one that allowed the specific design I had in mind. I'll still mention it here as a partial answer in case it helps someone in the future. For more info, check this other answer on that specific question

public class SomeClass<Derived> 
    where Derived : class
{
    public void call<Parent>()
        where Parent : class
    {
        if(typeof(Derived).IsSubclassOf(typeof(Parent)))
        {
            //do your stuff
        }
        else
        {
            throw new Exception("Must be called wih parent classes only!");
        }
    }
}

The compiler is getting confused here with actual type name being passed:

public class ParenthoodChecker<Derived, **Parent**> 
        where Parent : class 
        where Derived : Parent

    {
        public ParenthoodChecker()
        { }
    }

We normally would pass a generic type and specify the constraint of the type with where clause.

Try this:

 public class ParenthoodChecker<Derived, TParent>        
        where Derived : Parent
        where TParent : class 
    {
        public ParenthoodChecker()
        { }
    }

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