2

I am trying to implement a generic method but need to cast an object of one generic type (T1 - defined on the class) to another generic type (T2 defined on the method). Other than defining T2 on the class (which I'd rather not because it won't always be needed), is there a way of achieving this?

I'm after something like this:

public class SomeClass<T1>
{
    public void SomeMethod<T2>(T1 someParameter) where T1 : T2
    {
        T2 someVariable = (T2) someParameter;
    }
}

It seems the constraint will only work the wrong way around, that is where T2:T1 works (but is obviously wrong for my purpose), but where T1:T2 doesn't.

update The reason I need to cast T1 to T2, is I use the result in a database insert method which uses reflection on the Interface to determine what columns to insert into. The interface is used to prevent trying to insert into computed columns for instance. So T2 would be this interface whereas T1 would be an original object (which would have more fields). Hence casting T2:T1 would not be correct.

jem
  • 61
  • 8
  • 2
    If `T1 : T2` then you would not need the cast at all. Not sure what you're trying to achieve here. – Matthew Watson May 06 '21 at 15:41
  • 1
    maybe you mean `where T2 : T1`? so that `T2` is an unknown type that derives from `T1`? – Dave Cousineau May 06 '21 at 15:54
  • @MatthewWatson - please see my update. The cast (or at least the constraint) is required to ensure that only the correct fields get uses later down the line. – jem May 06 '21 at 15:54
  • @DaveCousineau thanks for the input, but as described in my update, T2 is a subset of the fields on T1 so it is T1 that derives from T2, not the other way around. – jem May 06 '21 at 15:56
  • Side note about the bigger picture: Are you trying to create a wrapper around an ORM (maybe EF)? IE a new abstraction? Or is this a custom db wrapper you are creating or modifying? – Igor May 06 '21 at 15:56
  • 1
    @Igor nearly. We have a wrapper around Dapper, which makes Inserts and Updates simpler. This is implementing a method where I will be doing several very similar sorts of updates, with just the table names and columns changed. The types define which columns I'll be inserting into. – jem May 06 '21 at 15:59
  • @jem Here T1 is a generic parameter available for the whole class and so for the method. But T2 is another generic parameter that applies only for this method. Therefore T1 is already in use and you can't constraint T1 to be of T2... thus the @.jamiec answer's hack. See [C# Generic Classes](https://docs.microsoft.com/dotnet/csharp/programming-guide/generics/generic-classes) & [Generics open and closed constructed types](https://stackoverflow.com/questions/1735035/generics-open-and-closed-constructed-types) & [Constructed Types](https://www.informit.com/articles/article.aspx?p=1648574&seqNum=4) –  May 06 '21 at 16:46

3 Answers3

2

You can do this fine* with is (or as) keyword - the only constraint nexcessary on T2 is that it is a class

public class SomeClass<T1>
{
    public void SomeMethod<T2>(T1 someParameter) where T2 : class
    {
        if(!(someParameter is T2 t2))
        {           
            throw new Exception("Invalid type");
        }
        Console.WriteLine($"Hello from {t2}");
    }
}

Live example: https://dotnetfiddle.net/C5Brhn

(* fine, although it is a runtime check, not the compile-time check your original question hinted at)

With .NET5 you can use a better expression is not

public class SomeClass<T1>
{
    public void SomeMethod<T2>(T1 someParameter) where T2 : class
    {
        if(someParameter is not T2 t2)
        {           
            throw new Exception("Invalid type");
        }
        Console.WriteLine($"Hello from {t2}");
    }
}

Note that based on this comment

T2 is a subset of the fields on T1 so it is T1 that derives from T2

I have used T1=Lion and T2=Animal

Jamiec
  • 133,658
  • 13
  • 134
  • 193
  • Thanks @Jamiec, this is what I've gone for. In reality the method and class was somewhat more complex and my someParameter was actually an IReadonlyCollection of T1, so the `someParameter is not T2 t2` check would need to be done on the first item of that collection (unless there is another way of checking the type without instantiating it?) – jem May 06 '21 at 16:52
  • 1
    "You can do this fine" - this should *at least* be qualified with a warning that what you're describing here is merely a runtime check, in contrast to the compile time check outlined in the question. – O. R. Mapper May 07 '21 at 16:26
  • @O.R.Mapper That's a really good point, I have included that in the answer. – Jamiec May 07 '21 at 17:30
1

So, you want a type that is a base of T1. You can't do that directly, but you could do the following, though you would have to prove the relationship when using the method by specifying the type arguments. There may also be ways to defeat the constraint.

Basically, introduce a type parameter that "is a" T1, and then you can introduce a type parameter that is a base of that type. This ends up looking like this:

public class SomeClass<TOriginal> {
   public void SomeMethod<TSubstitute, TBase>(
      TOriginal someParameter
   ) where TSubstitute : TOriginal, TBase {
        TBase someVariable = (TBase)(TSubstitute)someParameter;

        ...
    }
}

When using it, TOriginal and TSubstitute will usually be the same type. This doesn't explicitly say that TOriginal is descended from TBase, but it does say there exists some other type TSubstitute that is descended from both (and 'descended from' includes being the same type).

Dave Cousineau
  • 12,154
  • 8
  • 64
  • 80
0

It sounds like you want to ensure that T2 is in some way castable or convertible to T1, you may want to consider the use of a common interface between the two objects as a constraint for SomeMethod.

Heres a short example:

public interface ISomeInterface
{

}

public class SomeBaseClass : ISomeInterface
{

}

public class SomeClass<T1> where T1 : SomeBaseClass
{
    public void SomeMethod<T2>(T1 someParameter) where T2 : ISomeInterface
    {
        if (someParameter is T2 commonInterfacedObject)
        {
            T2 someVariable = commonInterfacedObject;
        }
    }
}
DekuDesu
  • 2,224
  • 1
  • 5
  • 19
  • What is the point in a common interface if tyou never use it? – Jamiec May 06 '21 at 16:09
  • 1
    I could see a point in a common interface, but it feels a bit messy. Each table would need a common interface, and that would need to be added as a type argument to SomeClass. It also wouldn't prevent the types being specified the wrong way round. – jem May 06 '21 at 16:49