【问题标题】:Restricting a generic type parameters to have a specific constructor限制泛型类型参数以具有特定的构造函数
【发布时间】:2012-03-16 16:45:01
【问题描述】:

我想知道为什么对泛型类型参数的新约束只能在没有参数的情况下应用,也就是说,可以约束类型具有无参数构造函数,但不能约束类具有,例如,接收 int 作为参数的构造函数。我知道解决这个问题的方法,使用反射或工厂模式,可以正常工作。但我真的很想知道为什么,因为我一直在考虑它,我真的想不出无参数构造函数和带有参数的构造函数之间的区别,可以证明对新约束的这种限制是合理的。我错过了什么? 非常感谢


论据 1:构造函数是方法

@Eric:让我陪你一会儿:

构造函数是方法

那么我想如果我这样去没有人会反对:

public interface IReallyWonderful
{
    new(int a);

    string WonderMethod(int a);
}

但是一旦我有了它,我就会去:

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

这是我最初想做的事情。所以,很抱歉,但不,构造函数不是方法,或者至少不完全是。

关于实施此功能的困难,我真的不知道,即使我知道,我也不会对明智地支出股东资金的决定发表任何意见。类似的东西,我会立即标记为答案。

从学术(我)的角度来看,也就是说,不考虑实施成本,问题确实是(我在过去几个小时内将其四舍五入):

应将构造函数视为类实现的一部分,还是应视为语义契约的一部分(就像将接口视为语义契约一样)。

如果我们将构造函数视为实现的一部分,那么约束泛型类型参数的构造函数并不是一件非常通用的事情,因为这会将您的泛型类型绑定到具体的实现,并且几乎可以说为什么要使用泛型?

作为实现的一部分的构造函数示例(将以下任何构造函数指定为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));
    }
}

问题在于构造函数也可能被视为语义契约的一部分,如下所示:

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

    void Add(T tee);

    ...
}

这意味着,从一系列元素构建一个集合总是可行的,对吧?这将成为语义契约的一个非常有效的部分,对吧?

我在不考虑明智地使用股东资金的任何方面的情况下,倾向于允许构造函数作为语义合同的一部分。一些开发人员把它搞砸了,并限制某种类型具有一个语义不正确的构造函数,那么,同一个开发人员添加一个语义不正确的操作有什么区别?毕竟,语义契约就是这样,因为我们都同意它们是,而且因为我们都很好地记录了我们的库;)


论据 2:解析构造函数时的假设问题

@supercat 一直在尝试设置一些示例(引用评论)

如果不导致令人惊讶的行为,也很难准确定义构造函数约束应该如何工作。

但我真的不同意。在 C# 中(好吧,在 .NET 中)诸如“如何让企鹅飞起来?”之类的惊喜。根本不会发生。关于编译器如何解析方法调用有一些非常简单的规则,如果编译器无法解析它,那么它就不会通过,也不会编译。

他的最后一个例子是:

如果它们是逆变的,那么如果泛型类型具有约束 new(Cat, ToyotaTercel) 而实际类型只有构造函数 new(Animal, ToyotaTercel) 和 new (猫,汽车)。

好吧,让我们试试这个(我认为这与@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
{ }

而且,哇,它不会因为错误而编译

以下方法或属性之间的调用不明确:“TestApp.Program.FunnyMethod(TestApp.Animal, TestApp.ToyotaTercel)”和“TestApp.Program.FunnyMethod(TestApp.Cat, TestApp.Automobile)”

如果相同的问题从具有参数化构造函数约束的解决方案中引起,我不明白为什么结果应该不同,如下所示:

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)
{ }

现在,当然,编译器无法处理构造函数上的约束,但如果可以,为什么错误不会出现在GenericClass&lt;FunnyClass&gt; gc = new GenericClass&lt;FunnyClass&gt;(); 行上,类似于尝试编译第一个示例时获得的错误,FunnyMethod 的那个。

无论如何,我会更进一步。当重写抽象方法或实现定义在接口上的方法时,需要使用完全相同的参数类型来执行此操作,不允许继承者或祖先。因此,当需要参数化构造函数时,应通过精确定义满足要求,而不是其他任何东西。在这种情况下,对于类GenericClass 的泛型参数类型,永远不能将类FunnyClass 指定为类型。

【问题讨论】:

  • 查看 Lippert 的 cmets herehere
  • Lippert:“我经常被问到为什么编译器不实现这个或那个特性,当然答案总是一样的:因为没有人实现它。特性开始时是未实现的只有当人们努力实现它们时才会被实现:不努力,没有特性。这当然是一个不令人满意的答案,因为通常提出问题的人已经假设该特性是如此明显地好,以至于我们需要有一个理由不实现它。实际上,无论多么明显的好,我们都不需要理由不实现任何功能。”
  • 是的,没错,很明显。你是说这个功能不存在仅仅是因为没有人实现它吗?也就是说,实现它并不意味着在语言中引入 somo 不一致或矛盾,在这方面,无参数构造函数和其他构造函数之间确实没有区别。对?如果是这样,对我来说似乎有点随机,一直执行new () 约束,然后将其留在那里。但话又说回来,如果你发明了 .NET,你就会按照你想要的方式来发明它,对吧?
  • @PaulieWaulie:明智地花费股东的钱不是懒惰。

标签: c# generics constraints new-operator


【解决方案1】:

Kirk Woll 对我的引用当然是所有必要的理由;我们不需要为现有的功能提供理由。功能具有巨大的成本。

但是,在这种特定情况下,我当然可以为您提供一些理由,说明如果该功能在设计会议上作为该语言未来版本的可能功能出现,我会否决该功能。

首先:考虑更一般的功能。构造函数是方法。如果您希望有一种方法可以说“类型参数必须有一个接受 int 的构造函数”,那么为什么说“类型参数必须有一个名为 Q 的公共方法,它接受两个整数并返回一个字符串?”

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

你觉得这是一件非常通用的事情吗?有这种约束似乎与泛型的想法背道而驰。

如果该特性对于方法来说是个坏主意,那么为什么对于恰好是构造函数的方法是一个好主意

相反,如果方法和构造函数是个好主意,那为什么要停在那里呢?

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

我说我们应该做整个功能根本不做。如果能够将类型限制为具有特定构造函数很重要,那么让我们完成整个功能并根据一般成员而不只是来限制类型>构造函数

当然,该功能的设计、实施、测试、记录和维护成本要高得多。

第二点:假设我们决定实现这个特性,要么是“just constructors”版本,要么是“any member”版本。 我们生成什么代码? 通用代码生成器的特点是它经过精心设计,您可以进行一次静态分析并完成它。但是在 IL 中没有标准的方式来描述“调用采用 int 的构造函数”。我们将不得不向 IL 添加一个新概念,或者生成代码以便通用构造函数调用使用反射

前者贵;在 IL 中改变一个基本概念是非常昂贵的。后者是 (1) 慢,(2) 将参数框起来,(3) 是您可以自己编写的代码。如果您打算使用反射找到构造函数并调用它,那么编写使用反射找到构造函数并调用它的代码。如果这是代码生成策略,那么约束赋予的唯一好处是,传递一个没有采用 int 的公共 ctor 的类型参数的错误在编译时而不是运行时被捕获。您不会获得泛型的任何其他好处,例如避免反射和拳击惩罚。

【讨论】:

  • 但是您已经可以通过使用接口约束来限制一个类型具有某种方法。我意识到这与您所说的一般方法约束不同,但几乎一样好。另一方面,没有简单的基于构造函数的限制方法。
  • 它并没有那么可怕 - 与其向 IL 添加任何内容,不如让 C# 编译器使用 F# 用于该功能的相同元数据格式(任意成员约束)。 F# 编译器避免装箱和反射,但要求在所有调用点内联“通用”方法的主体。当然,这并不是说考虑到 C# 团队的优先级,这对 C# 来说是一个合适的特性。只是它不一定是一项艰巨的任务。
  • “你觉得这是一件非常通用的事情吗?拥有这种约束似乎与泛型的想法背道而驰。” ——似乎一点也不反;例如“static T Parse(string)”在各种类中实现,但它不是从任何基类派生的。为什么我不能强制执行“where T: has static T Parse(string)”?更不用说约束检查是在编译时完成的,而且 IL 代码生成不是问题,因为我们有 DLR 和反射。那就是“我想要任何具有'public static T Parse(string)'方法的类”仍然非常通用。
  • 我不完全理解这个反对意见。为什么原作者需要了解其泛型参数被限制继承的类型的派生类?实现泛型类的人只需要知道我们假设参数的基类型的公共构造函数是什么样的以及它的用途。如果您在计算机上输入class MyGeneric&lt;T&gt; where T : TWithConstructor,可以合理地假设您知道TWithConstructor 是什么,它暴露的构造函数是什么样的,以及您需要传递它,不是吗?
  • @EricLippert 如果您有new()-约束,将为T M() { return new T(); } 生成什么IL-Body?除了构造函数调用之外,任何其他方法调用的 IL 代码是什么样的? (对不起,当我挖出一年前的讨论时)
【解决方案2】:

总结

这是试图捕获有关此问题的当前信息和解决方法并将其作为答案呈现。

我发现泛型与约束相结合是 C# 最强大和最优雅的方面之一(来自 C++ 模板背景)。 where T : Foo 很棒,因为它为 T 引入了功能,同时在编译时仍将其限制为 Foo。在许多情况下,它使我的实现更简单。起初,我有点担心以这种方式使用泛型类型会导致泛型在代码中增长,但我允许它这样做,而且好处大大超过了任何缺点。但是,在构造带参数的泛型类型时,抽象总是失败。

问题

当约束一个泛型类时,你可以指出泛型必须有一个无参数的构造函数,然后实例化它:

public class Foo<T>
   where T : new()
{
    public void SomeOperation()
    {
        T something = new T();
        ...
    }
}

问题是只能限制无参数构造函数。这意味着下面建议的解决方法之一需要用于具有参数的构造函数。如下所述,这些变通方法有一些缺点,从需要额外的代码到非常危险。另外,如果我有一个类,它有一个由泛型方法使用的公共无参数构造函数,但是在某个地方该类被更改,因此构造函数现在有一个参数,那么我需要更改模板的设计和周围使用其中一种解决方法而不是 new() 的代码。

这是 Microsoft 绝对知道的事情,请参阅 Microsoft Connect 上的这些链接作为示例(不包括困惑的 Stack Overflow 用户提问)hereherehereherehere@ 987654326@here.

它们都以“不会修复”或“按设计”关闭。可悲的是,问题随后被锁定,不再可能投票给他们。但是,您可以为构造函数功能投票 here

解决方法

有三种主要的解决方法,但都不是理想的:-

  1. Use Factories。这需要大量样板代码和开销
  2. Use Activator.CreateInstance(typeof(T), arg0, arg1, arg2, ...)。这是我最不喜欢的,因为类型安全性丢失了。如果你在 T 类型的构造函数中添加一个参数怎么办?您会遇到运行时异常。
  3. Use the Function/action approach.here。这是我最喜欢的,因为它保留了类型安全性并且需要更少的样板代码。然而,它仍然不像 new T(a,b,c) 那样简单,并且作为一个泛型抽象通常跨越许多类,知道类型的类通常与需要实例化它的类相距几个类,以便func 被传递导致不必要的代码。

解释

Microsoft Connect 上提供了标准响应:

“感谢您的建议。微软已经收到了一些 关于更改泛型类型的约束语义的建议, 以及在这方面做自己的工作。然而此时 Microsoft 无法保证该领域的变化将 成为未来产品版本的一部分。您的建议将被记录到 帮助推动该领域的决策。 同时下面的代码示例..."

该解决方法实际上不是我推荐的所有选项的解决方法,因为它不是类型安全的,并且如果您碰巧向构造函数添加另一个参数会导致运行时异常。

Eric Lippert 在this very stack overflow post 中提供了我能找到的最佳解释。 我非常感谢这个答案,但我认为需要在用户层面和技术层面对此进行进一步讨论(可能是比我更了解内部的人)。

我最近还发现 Mads Torgersen in this link 有一个很好的细节(请参阅“Microsoft Posted by Microsoft on 3/31/2009 at 3:29 PM”)。

问题在于构造函数与其他方法不同,因为我们已经可以通过派生约束(接口或基类)尽可能多地约束方法。可能在某些情况下方法约束是有益的,我从来不需要它们,但是我不断地遇到无参数构造函数的限制。当然,general (not constructor-only) solution 是理想的选择,Microsoft 需要自行决定。

建议

关于有争议的好处与实施难度,我可以理解这一点,但会提出以下几点:-

  1. 在编译时而不是运行时(在这种情况下为类型安全)捕获错误非常有价值。
  2. 似乎还有其他不那么可怕的选择。有一些关于如何实施的建议。值得注意的是,Jon Skeet 提出了“静态接口”作为解决此问题的一种方法,似乎 CLR 中已经存在显式成员约束,但 C# 中不存在,请参阅 cmets here 和讨论 here。此外,kvb in Eric Lippert's response 对任意成员约束的评论。

状态

据我所知,不会以任何形式发生。

【讨论】:

    【解决方案3】:

    如果想要有一个泛型类型T 的方法,其实例可以使用单个int 参数创建,除了类型T 之外,还应该有方法accept,或者Func&lt;int, T&gt;或者一个适当定义的接口,可能使用类似的东西:

    static class IFactoryProducing<ResultType>
    {
        interface WithParam<PT1>
        {
            ResultType Create(PT1 p1);
        }
        interface WithParam<PT1,PT2>
        {
            ResultType Create(PT1 p1, PT2 p2);
        }
    }
    

    (如果可以将外部静态类声明为接口,则代码看起来会更好,但IFactoryProducing&lt;T&gt;.WithParam&lt;int&gt; 似乎比IFactory&lt;int,T&gt; 更清晰(因为后者对于参数的类型和结果的类型不明确) .

    在任何情况下,每当一个传递类型 T 时,一个也传递一个合适的工厂委托或接口,一个可以实现参数化构造函数约束可以实现的 99%。通过让每个可构造类型生成工厂的静态实例,可以最大限度地降低运行时成本,因此无需在任何类型的循环上下文中创建工厂实例。

    顺便说一句,除了该功能的成本之外,几乎可以肯定会有一些实质性的限制,这将使它不如解决方法通用。如果构造函数约束在参数类型方面不是逆变的,则除了要使用的参数的实际类型之外,可能需要传递构造函数约束所需的确切类型的类型参数;当一个人这样做的时候,一个人还不如绕过一家工厂。如果它们是逆变的,那么如果泛型类型具有约束new(Cat, ToyotaTercel),而实际类型只有构造函数new(Animal, ToyotaTercel)new(Cat, Automobile),那么在解析应该调用哪个构造函数时会遇到麻烦。

    PS--为了澄清问题,逆变构造函数约束导致“双菱形”问题的变体。考虑:

    T CreateUsingAnimalAutomobile<T>() where T:IThing,new(Animal,Automobile)
    { ... }
    
    T CreateUsingAnimalToyotaTercel<T>() where T:IThing,new(Animal,ToyotaTercel)
    { return CreateUsingAnimalAutomobile<T>(); }
    
    T CreateUsingCatAutomobile<T>() where T:IThing,new(Cat,Automobile)
    { return CreateUsingAnimalAutomobile<T>(); }
    
    IThing thing1=CreateUsingAnimalToyotaTercel<FunnyClass>(); // FunnyClass defined in question
    IThing thing2=CreateUsingCatAutomobile<FunnyClass>(); // FunnyClass defined in question
    

    在处理对CreateUsingAnimalToyotaTercel&lt;FunnyClass&gt;() 的调用时,“Animal,ToyotaTercel”构造函数应满足该方法的约束,该方法的泛型类型应满足CreateUsingAnimalAutomobile&lt;T&gt;() 的约束。在处理对CreateUsingCatAutomobile&lt;FunnyClass&gt;() 的调用时,“Cat,Automobile”构造函数应满足该方法的约束,该方法的泛型类型应满足CreateUsingAnimalAutomobile&lt;T&gt;() 的约束。

    问题是这两个调用都会调用同一个CreateUsingAnimalAutomobile&lt;SillyClass&gt;() 方法,而该方法无法知道应该调用哪个构造函数。与逆变相关的歧义并不是构造函数独有的,但在大多数情况下,它们是通过编译时绑定解决的。

    【讨论】:

    • 是的,我知道解决方法并使用过它们。我只是想知道是否存在没有参数化构造函数约束的概念性原因,例如“看,你在这里没有考虑到这种情况,具有参数化构造函数约束可能会将你的应用程序送入地狱”,或者, “不,没有任何理由,只是成本太高或太长”。谢谢
    • 存在变通方法的事实意味着实现该功能的回报有限。在不导致令人惊讶的行为的情况下,也很难准确定义构造函数约束应该如何工作。
    • 属性的解决方法也存在,而且非常简单,比这种解决方法(getter 和 setter 方法)要简单得多,但这并不意味着它们没有得到实现。所以,这不是一个真正的答案。毕竟,也有使用 .NET 开发的解决方法(例如使用 Java),这并不意味着 .NET 没有得到实现,感谢上帝,对吧?
    • 至于你上面的以动物和汽车为参数的构造函数的例子,好吧,我认为你的例子是错误的,如果实际类型只有一个构造函数,真的没有什么要解决的,唯一的构造函数may be called 是唯一的构造函数。无论如何,我认为这真的很简单,假设解析构造函数以应用与应用于相同命名方法的规则完全相同的规则,真的,看不到那里的问题。
    • @amandatarafa:我的例子是向后的——固定的。您在谈论哪种属性的解决方法?我真的很想看到一个很好的方法,可以将MyCollectionOfPoint[whatever].X += 5 之类的东西翻译成MyCollectionOfPoint.ActOnItem(whatever, (ref Point item) =&gt; item.X += 5) 之类的东西,让集合有机会以它认为合适的方式包装更新。即使集合包含类项而不是结构,找出它们何时更新的唯一方法是使用各种棘手的回调或事件逻辑。
    猜你喜欢
    • 1970-01-01
    • 2023-02-02
    • 1970-01-01
    • 1970-01-01
    • 2021-06-27
    • 1970-01-01
    • 1970-01-01
    • 2011-12-26
    • 1970-01-01
    相关资源
    最近更新 更多