简体   繁体   中英

How to implement a generics interface without specific types?

Given an interface like this:

public interface Transformer<TSource, TResult> {
    TResult Transform(TSource original);
}

I want to provide a simple "no-op" implementation of that interface for cases where no transformation is needed, which would just return the original object itself. In that case, TSource and TResult would be the same.

Being an old Java dude who's still learning C#, my first though was just this:

public class NoopTransformer : Transformer<object, object> {
    public override object Transform(object original) {
        return original;
    }
}

That compiles but I can't practically use it anywhere that just expects a Transformer . If I try, I get compile errors that say NoopTransformer can't be used as a Transformer<TSource, TResult>

What I long for is Java's wildcards or willingness to treat Object as an acceptable type parameter for any object. Is there really no equivalent in C#?


Then I thought this would work:

public class NoopTransformer<TSource> : Transformer<TSource, TSource> {
    public override TSource Transform(TSource original) {
        return entity;
    }
}

That doesn't work in any place that expects a Transformer<TSource, TResult> , like this:

public class Controller<TEntity, TResult> {
    private Transformer<TEntity, TResult> transformer;

    public Controller() : this(new NoopTransformer<TEntity>()) // error on this line
        { }

    public Controller(Transformer<TEntity, TResult> transformer) {
        this.transformer = transformer;
    }
}

That fails to compile, saying,

  • Cannot convert from NoopTransformer<TEntity> to Transformer<TSource, TResult>
  • Type TEntity doesn't match the expected type TResult.
  • Argument type NoopTransformer<TEntity> is not assignable to parameter type Transformer<TEntity, TResult>

(even though it seems obvious that my impl satisfies that contract - the fact that TSource and TResult are the same shouldn't matter).


Finally, grasping at straws, I tried this:

public class NoopTransformer<TSource, TResult> : Transformer<TSource, TResult>
    where TResult : TSource
{
    public override TResult Transform(TSource original) {
        return (TResult)entity;
    }
}

But of course that doesn't work because where I want to use this, there is no constraint that says TResult must extend TSource .


The context is that I have another class that has the same TSource and TResult type parameters, and can accept a Transformer in its constructor. But the transformer is optional in that class; I want to use NoopTransformer in cases where the client doesn't specify a more specific Transformer .

In Java this is trivial, in fact with a couple of different ways to solve it (using type wildcards, extends , etc). I realize that Java is more lenient (due to its use of type erasure), and so is inherently more flexible since there are no type parameters at runtime. But it seems that the C# compiler is being unnecessarily strict (and maybe a little dumb).

How can I achieve my goal of having a useful default for the interface?

The compiler is correct to reject

public Controller() : this(new NoopTransformer<TEntity>())

since a Transformer<TEntity, TEntity> is not compatible with Transformer<TEntity, TResult> since TEntity and TResult are not guaranteed to be the same eg

var c = new Controller<string, int>();

You can maintain static safety by creating a factory method for constructing a Controller instance where TEntity and TResult are the same:

public static class Controller
{
    public static Controller<TEntity, TEntity> Create<TEntity>()
    {
        return new Controller<TEntity, TEntity>(new IdentityTransformer<TEntity>());
    }
}

if for some reason you cannot adopt this approach you will have to rely on casting at runtime:

public class CastingTransformer<TSource, TResult> : Transformer<TSource, TResult>
{
    public TResult Transform(TSource original)
    {
        return (TResult)(object)original;
    }
}

public Controller() : this(new CastingTransformer<TEntity, TResult>()) { }

Is this what you had in mind?

public interface Transformer<TSource, TResult>
{
    TResult Transform(TSource original);
}

public class NoopTransformer<TSource, TResult> : Transformer<TSource, TResult> where TResult : class
{
    public TResult Transform(TSource original)
    {
        return original as TResult;
    }
}

public class Controller<TEntity, TResult> where TResult : class
{
    private Transformer<TEntity, TResult> transformer;

    public Controller() : this(new NoopTransformer<TEntity, TResult>())
    { }

    public Controller(Transformer<TEntity, TResult> transformer)
    {
        this.transformer = transformer;
    }
}

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