Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
377 views
in Technique[技术] by (71.8m points)

c# - Restricting a generic type parameters to have a specific constructor

I'd like to know why the new constraint on a generic type parameter can only be applied without parameters, that is, one may constraint the type to have the parameterless constructor, but one cannot constraint the class to have, say, a constructor that receives a int as a parameter. I know ways around this, using reflection or the factory pattern, that works fine, ok. But I'd really like to know why, because I've been thinking about it and I really can't think of a difference between a parameterless constructor and one with parameters that would justify this restriction on the new constraint. What am I missing? Thanks a lot


Argument 1: Constructors are methods

@Eric: Let me go here with you for a sec:

Constructors are methods

Then I suppose no one would object if I'd go like this:

public interface IReallyWonderful
{
    new(int a);

    string WonderMethod(int a);
}

But once I have that, then I'd go:

public class MyClass<T>
        where T : IReallyWonderful
{
    public string MyMethod(int a, int b)
    {
        T myT = new T(a);
        return myT.WonderMethod(b);
    }
}

Which is what I wanted to do in the first place. So, sorry, but no, constructors are not methods, or at least not exactly.

On the difficulties of implementing this feature, well I'd really wouldn't know, and even if I did, I wouldn't have anything to say on a decision regarding the wisely expenditure of shareholder money. Something like that, I would've marked as an answer right away.

From an academic (my) point of view, and that is, without any regards for implementation costs, the question really is (I've rounded it up to this in the last few hours):

Should constructors be considered as part of the implementation of a class, or as part of the semantic contract (in the same way an interface is considered a semantic contract).

If we consider constructors as part of the implementation, then, constraining the constructor of a generic type parameter is not a very generic thing to do, since that'd be tying up your generic type to a concrete implementation, and one almost could say why use generics at all?

Example of constructor as part of the implementation (no sense in specifying any of the following constructors as part of the semantic contract defined by ITransformer):

public interface ITransformer
{
    //Operates with a and returns the result;
    int Transform(int a);
}

public class PlusOneTransformer : ITransformer
{
    public int Transform(int a)
    {
        return a + 1;
    }
}

public class MultiplyTransformer : ITransformer
{
    private int multiplier;

    public MultiplyTransformer(int multiplier)
    {
        this.multiplier = multiplier;
    }

    public int Transform(int a)
    {
        return a * multiplier;
    }
}

public class CompoundTransformer : ITransformer
{
    private ITransformer firstTransformer;
    private ITransformer secondTransformer;

    public CompoundTransformer(ITransformer first, ITransformer second)
    {
        this.firstTransformer = first;
        this.secondTransformer = second;
    }

    public int Transform(int a)
    {
        return secondTransformer.Transform(firstTransformer.Transform(a));
    }
}

The problem is that constructors may also be considered as part of the semantic contract, like so:

public interface ICollection<T> : IEnumerable<T>
{
    new(IEnumerable<T> tees);

    void Add(T tee);

    ...
}

This means, it's always posible to build a collection from a sequence of elements, right? And that would make a very valid portion of a semantic contract, right?

Me, without taking into account any of the aspects regarding the wisely expenditure of shareholder money, would favour allowing constructors as parts of semantic contracts. Some developer messes it up and constraints a certain type to having a semantically incorrect constructor, well, what's the difference there from the same developer adding a semantically incorrect operation? After all, semantic contracts are that, because we all agreed they are, and because we all document our libraries really well ;)


Argument 2: Supposed problems when resolving constructors

@supercat is been trying to set some examples as how (quote from a comment)

It would also be hard to define exactly how constructor constraints should work, without resulting in surprising behaviors.

but I really must disagree. In C# (well, in .NET) surprises like "How to make a penguin fly?" simply don't happen. There are pretty straightforward rules as to how the compiler resolves method calls, and if the compiler can't resolve it, well, it won't pass, won't compile that is.

His last example was:

If they are contravariant, then one runs into trouble resolving which constructor should be called if a generic type has constraint new(Cat, ToyotaTercel), and the actual type just has constructors new(Animal, ToyotaTercel) and new(Cat, Automobile).

Well, lets try this (which in my opinion is a similar situation to that proposed by @supercat)

class Program
{
    static void Main(string[] args)
    {
        Cat cat = new Cat();
        ToyotaTercel toyota = new ToyotaTercel();

        FunnyMethod(cat, toyota);
    }

    public static void FunnyMethod(Animal animal, ToyotaTercel toyota)
    {
        Console.WriteLine("Takes an Animal and a ToyotaTercel");
    }

    public static void FunnyMethod(Cat cat, Automobile car)
    {
        Console.WriteLine("Takes a Cat and an Automobile");
    }
}

public class Automobile
{ }

public class ToyotaTercel : Automobile
{ }

public class Animal
{ }

public class Cat : Animal
{ }

And, wow, it won't compile with the error

The call is ambiguous between the following methods or properties: 'TestApp.Program.FunnyMethod(TestApp.Animal, TestApp.ToyotaTercel)' and 'TestApp.Program.FunnyMethod(TestApp.Cat, TestApp.Automobile)'

I don't see why the result should be different if the same probleme arouse out of a solution with parameterized constructor constraints, like so:

class Program
{
    static void Main(string[] args)
    {
        GenericClass<FunnyClass> gc = new GenericClass<FunnyClass>();
    }
}

public class Automobile
{ }

public class ToyotaTercel : Automobile
{ }

public class Animal
{ }

public class Cat : Animal
{ }

public class FunnyClass
{
    public FunnyClass(Animal animal, ToyotaTercel toyota)
    {            
    }

    public FunnyClass(Cat cat, Automobile car)
    {            
    }
}

public class GenericClass<T>
   where T: new(Cat, ToyotaTercel)
{ }

Now, of course, the compiler can't handle the constraint on the constructor, but if it could, why could't the error be, on the line GenericClass<FunnyClass> gc = new GenericClass<FunnyClass>(); similar to that obtained when trying to compile the first example, that of the FunnyMethod.

Anyway, I'd go one step further. When one overrides an abstract method or implements a method defined on an interface, one is required to do so with exactly the same parameters type, no inheritors or ancestors allowed. So, when a parameterized constructor is required, the requirement should be met with an exact definition, not with anything else. In this case, the class FunnyClass could never be specified as the type, for the generic parameter type of class GenericClass.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

Kirk Woll's quote from me of course is all the justification that is required; we are not required to provide a justification for features not existing. Features have enormous costs.

However, in this specific case I can certainly give you some reasons why I would push back on the feature if it came up in a design meeting as a possible feature for a future version of the language.

To start with: consider the more general feature. Constructors are methods. If you expect there to be a way to say "the type argument must have a constructor that takes an int" then why is it not also reasonable to say "the type argument must have a public method named Q that takes two integers and returns a string?"

string M<T>(T t) where T has string Q(int, int)
{
    return t.Q(123, 456);
}

Does that strike you as a very generic thing to do? It seems counter to the idea of generics to have this sort of constraint.

If the feature is a bad idea for methods, then why is it a good idea for methods that happen to be constructors?

Conversely, if it is a good idea for methods and constructors, then why stop there?

string M<T>(T t) where T has a field named x of type string
{
    return t.x;
}

I say that we should either do the whole feature or don't do it at all. If it is important to be able to restrict types to have particular constructors, then let's do the whole feature and restrict types on the basis of members in general and not just constructors.

That feature is of course a lot more expensive to design, implement, test, document and maintain.

Second point: suppose we decided to implement the feature, either the "just constructors" version or the "any member" version. What code do we generate? The thing about generic codegen is that it has been carefully designed so that you can do the static analysis once and be done with it. But there is no standard way to describe "call the constructor that takes an int" in IL. We would have to either add a new concept to IL, or generate the code so that the generic constructor call used Reflection.

The former is expensive; changing a fundamental concept in IL is very costly. The latter is (1) slow, (2) boxes the parameter, and (3) is code that you could have written yourself. If you're going to use reflection to find a constructor and call it, then write the code that uses reflection to find a constructor and call it. If this is the code gen strategy then the only benefit that the constraint confers is that the bug of passing a type argument that does not have a public ctor that takes an int is caught at compile time instead of runtime. You don't get any of the other benefits of generics, like avoiding reflection and boxing penalties.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...