C# - How to instantiate an object with constraint from existing interface method without constraint
Asked Answered
V

3

5

I've searched and have found answers that make sense when you have access to change an existing interface definition. In my case, I do not have that ability. I'm trying to create a new object with a generic that has a constraint from a generic method that does not. The following code illustrates what I'm trying to do (I can do reflection or anything necessary in MyImplementedClass, but the others are more fixed):

// This existing interface has no constraint (can't change)
public interface IExistingInterface
{
    void ExistingMethod<T>();
}

public class MyImplementedClass : IExistingInterface
{
    public void ExistingMethod<T>()
    {
        var howToDoThis = new ExistingClass<T>(); // Gives error that T must be a reference type
    }
}

// This existing class has the following constraint (can't change)
public class ExistingClass<T> where T : class
{
    
}
Victorie answered 7/10, 2024 at 19:38 Comment(1)
How do you plan to handle (in terms of intended behavior) the case where the ExistingMethod<int>() is called? (imagine any other value type instead of int). For the code that works with an interface it is perfectly valid usage of the ExistingMethod as the interface has no restrictions/constraints here.Harem
S
7

One possibility would be to publicly implement a method on MyImplementedClass with the required constraint, then implement IExistingInterface.ExistingMethod<T>() explicitly, making it call the public method via MethodInfo.MakeGenericMethod(typeof(T)).Invoke():

public class MyImplementedClass : IExistingInterface
{
    void IExistingInterface.ExistingMethod<T>()
    {
        if (typeof(T).IsValueType)
        {
            // TODO: decide what to do when T is a value type
            throw new ArgumentException($"Invalid type {nameof(T)}");
        }
        else
        {
            var method = typeof(MyImplementedClass).GetMethod(nameof(MyImplementedClass.ExistingMethod));
            method.MakeGenericMethod(typeof(T)).Invoke(this, Array.Empty<object>());
        }
    }

    public void ExistingMethod<T>() where T : class
    {
        var howToDoThis = new ExistingClass<T>(); 
        Console.WriteLine($"Created {howToDoThis}");
    }
}

The advantages and disadvantages of this approach are:

  1. Inside the properly constrained public method you will be able to declare variables as ExistingClass<T> rather than just dynamic or object.

  2. Trying to directly call the properly constrained public method with a value type will result in a compile error. Trying to call the unconstrained directly implemented interface method will result in a runtime argument exception (or whatever else you choose to do in the //TODO).

  3. There will be no reflection penalty when the properly constrained method is called directly.

  4. By explicitly implementing the interface method, one nudges (but does not require) consumers of MyImplementedClass to call the properly constrained public method which provides for compile-time error checking rather than runtime error checking.

  5. However MethodInfo.MakeGenericMethod() is not guaranteed to work in Native AOT apps.

Demo fiddle here.

Superfluid answered 7/10, 2024 at 20:10 Comment(8)
don't you think that having a method with the same name as the interface one but with different constraints (semantics) can lead to a bit of confusion?Sententious
@IvanPetrov - maybe? At one company I worked at the CTO was always creating public new method with different constraints but identical names as base class + interface methods so we all got used to it. And while I did that in my code it isn't actually required by the answer, the answer only requires a constrained public method and an unconstrained explicit method. The main idea here is that, by explicitly implementing the interface method, one nudges consumers of MyImplementedClass to call the properly constrained public method.Superfluid
I think this implementation is so far seeming like the best fit for my needs, however I get an AmbiguousMatchException on the "var method =" line. How does yours distinguish between the 2 methods?Victorie
@Superfluid Thanks for the detailed reply. I was actually considering this too when I first wrote my answer, but somehow felt it was a "smell". Still don't like it, but you save yourself some reflection calls too, so a win there if nothing else.Sententious
@dbc, to answer my own question, I realized there isn't a reason the name of the method with the constraint needs to be the same as the interface, so I can just make it unique (basically like Ivan Petrov is doing)Victorie
@Victorie - that certainly works (and is preferred by Ivan Petrov), there's no need to make them identical. However I'm not seeing any AmbiguousMatchException because the explicitly implemented method is nonpublic and also has the name IExistingInterface.ExistingMethod, see the fiddle and also How can I access an explicitly implemented method using reflection?.Superfluid
@Victorie I also don't see any AmbiguousMatchException with dbc's solutionSententious
It's very possible my code is just slightly different and causes some ambiguity, but it's easy enough to circumvent.Victorie
S
4

You can do this with reflection:

public class MyImplementedClass : IExistingInterface {

    public void ExistingMethod<T>() {
        if (!typeof(T).IsValueType) {
            s_existingMethodClass.MakeGenericMethod(typeof(T)).Invoke(this, null);
            return;
        }
        // Exception / Other method for valuetypes?
    }

    private static MethodInfo s_existingMethodClass = typeof(MyImplementedClass)
        .GetMethod(nameof(ExistingMethodClass),
            BindingFlags.Instance | BindingFlags.NonPublic);

    private void ExistingMethodClass<T>()
        where T : class {
        var howToDoThis = new ExistingClass<T>();
    }
}
Sententious answered 7/10, 2024 at 20:7 Comment(8)
I agree, I think this is a pretty eloquent solution! I ended up accepting dbc's solution, but I ended up using kind of a hybrid of yours and theirs.Victorie
Looks fine to me also. You statically cached the MethodInfo s_existingMethodClass which is probably a good idea as well.Superfluid
@Victorie yes, I would accept dbc's solution too. A bit more detail and discussion. He got a downvote too (for no reason again).Sententious
@IvanPetrov I know the pain :) it is quite strange to receive such downvoting sometimes. Even more strange is that the least useful answer got spared.. That's why i try to be part of the opposite movement :)Boccherini
@MichałTurczyn before seeing dbc's downvote I thought it was something personal (3rd or 4th answer from last 5-6 to get anon downvote). This actually happened today too - Mark Gravell was caught in the crossfires. Pretty sure somebody proposed somewhere on Meta that downvotes should only happen with leaving a comment. Curious what the reasoning against it was.Sententious
It happened to me in other tags too (not only C# related) some time ago, that's why I gave up answering there. There was some posts on meta, this one for example, but it is not likely to get "mandatory comments for downote"Boccherini
@MichałTurczyn 513 upvotes (even though it's locked)...still nothing. Thicker skin training for free here on SO I guess.Sententious
@IvanPetrov Tell me about it :DBoccherini
A
0

I would just implement that restriction in the code and once ensured that constraint is met by the type, create appropriate type and create instance of that type

public class MyImplementedClass : IExistingInterface
{
    public void ExistingMethod<T>()
    {
        var isClass = typeof(T).IsClass;
        var isInterface = typeof(T).IsInterface;
        if (!isClass && !isInterface)
        {
            throw new InvalidOperationException();
        }

        var howToDoThis = Activator.CreateInstance(typeof(ExistingClass<>).MakeGenericType(typeof(T)));
    }
}
Alverta answered 7/10, 2024 at 19:56 Comment(2)
IsClass isn't equivalent to where: class. The latter allows interfaces.Sententious
@IvanPetrov That's details, but thank you, corrected :)Boccherini

© 2022 - 2025 — McMap. All rights reserved.