使用接口而不是一般约束类型的原因是什么


15

在支持通用类型参数(也称为类模板和参数多态性,尽管每个名称当然都带有不同含义)的面向对象的语言中,通常可以在类型参数上指定类型约束,以便将其约束从另一种类型。例如,这是C#中的语法:

//for classes:
class ExampleClass<T> where T : I1 {

}
//for methods:
S ExampleMethod<S>(S value) where S : I2 {
        ...
}

在那些接口所约束的类型上使用实际接口类型的原因是什么?例如,进行方法签名的原因是什么I2 ExampleMethod(I2 value)


4
类模板(C ++)是完全不同的东西,并且比普通的泛型要强大得多。即使具有泛型的语言为它们借用了模板语法。
Deduplicator

接口方法是间接调用,而类型方法可以是直接调用。因此,后者可能比前者快,并且在使用ref值类型参数的情况下,实际上可能会修改值类型。
user541686

@Deduplicator:考虑到泛型早于模板,因此我看不到泛型如何可以从模板中借用任何东西,无论是语法还是其他方式。
约尔格W¯¯米塔格

3
@JörgWMittag:我怀疑重复数据删除器可能是通过“支持泛型的面向对象语言”来理解“ Java和C#”,而不是“ ML和Ada”。然后,尽管并非所有具有泛型或参数多态性的语言都是从C ++借用的,但C ++对前者的影响是显而易见的。
史蒂夫·杰索普

2
@SteveJessop:ML,Ada,Eiffel,Haskell早于C ++模板,Scala,F#,OCaml紧随其后,但它们都不共享C ++的语法。(有趣的是,即使从C ++特别是模板大量借用的D也不共享C ++的语法。)我认为“ Java和C#”是“具有泛型语言”的狭义视图。
约尔格W¯¯米塔格

Answers:


21

使用参数版本可以

  1. 给该功能用户的更多信息
  2. 限制您可以编写的程序数量(免费的错误检查)

作为一个随机示例,假设我们有一种方法可以计算二次方程的根

int solve(int a, int b, int c) {
  // My 7th grade math teacher is laughing somewhere
}

然后,您希望它可以处理其他类型的数字,例如int。你可以写类似

Num solve(Num a, Num b, Num c){
  ...
}

问题是,这并没有说明您想要的内容。它说

给我任何3件与数字类似的东西(不一定以相同的方式),我会给你一些数字

我们不能这样做int sol = solve(a, b, c),如果abcint因为我们不知道该方法会返回一个int到底!如果我们想以更大的表达方式使用该解决方案,则会导致一些尴尬的跳舞,并垂头丧气,祈祷。

在函数内部,有人可能会给我们一个浮点数,一个bigint和一个度数,我们必须将它们相加并相乘。我们想静态地拒绝此操作,因为这3个类之间的操作将变得毫无用处。a.plus(b) = b.plus(a)度数是mod 360,因此不会发生类似的欢闹。

如果我们将参数多态性与子类型结合使用,则可以排除所有这些情况,因为我们的类型实际上表明了我们的意思

<T : Num> T solve(T a, T b, T c)

或用“如果给我某种数字类型,我可以用这些系数求解方程”。

这在很多其他地方也有涉及。的示例的另一个良好来源是函数,其抽象超过某种容器,丙氨酸的reversesortmap等。


8
总而言之,通用版本保证所有三个输入(和输出)将是同一类型的数字。
MathematicalOrchid

但是,当您不控制有问题的类型(因此无法向其添加接口)时,这就不够了。为了获得最大的通用性,您必须接受由参数类型(例如Num<int>)参数化的接口作为附加参数。您始终可以通过委托为任何类型实现接口。从本质上讲,这就是Haskell的类型类,除了使用起来非常繁琐之外,因为您必须显式传递接口。
2015年

16

在那些接口所约束的类型上使用实际接口类型的原因是什么?

因为那就是你所需要的...

IFoo Fn(IFoo x);
T Fn<T>(T x) where T: IFoo;

是两个截然不同的签名。第一个采用实现接口的任何类型,并且唯一保证的是返回值满足接口。

第二种方法采用实现接口的任何类型,并保证它将再次至少返回该类型(而不是满足限制性较小的接口)。

有时,您需要较弱的担保。有时您想要更强的一个。


您能否举一个如何使用较弱担保版本的示例?
GregRos

4
@GregRos-例如,在我编写的某些解析器代码中。我有一个带有Or两个Parser对象的函数(一个抽象基类,但是原理成立)并返回一个新对象Parser(但类型不同)。最终用户不应该知道或关心什么是具体类型。
Telastyn 2015年

在C#中,我想如果没有新的约束,几乎不可能返回除传入的T之外的T(没有反射痛苦),以及使强力保证本身毫无用处。
NtscCobalt 2015年

1
@NtscCobalt:将参数编程和接口通用编程结合使用时,它会更有用。例如LINQ一直在做什么(接受一个IEnumerable<T>,返回另一个IEnumerable<T>,例如实际上是一个OrderedEnumerable<T>
Ben Voigt

2

对方法参数使用受约束的泛型可以使方法的返回类型基于传入的事物的类型。在.NET中,它们还可以具有其他优点。其中:

  1. 可以将接受约束的泛型作为refout参数的方法传递给满足约束条件的变量。相比之下,具有接口类型参数的非泛型方法将仅限于接受该确切接口类型的变量。

  2. 具有泛型类型参数T的方法可以接受T的泛型集合。接受an的方法IList<T> where T:IAnimal将可以接受List<SiameseCat>,但是想要an的方法IList<Animal>将不能这样做。

  3. 约束有时可以根据通用类型(例如)指定接口where T:IComparable<T>

  4. 实现接口的结构在传递给接受约束的泛型参数的方法时可以保留为值类型,但在作为接口类型传递时必须装箱。这会对速度产生巨大影响。

  5. 通用参数可以具有多个约束,而没有其他方法可以指定“同时实现IFoo和IBar的某种类型”的参数。有时,这可能是一把双刃剑,因为接收到类型参数的代码IFoo将很难将其传递给这样一个期望双约束泛型的方法,即使所讨论的实例将满足所有约束。

如果在特定情况下使用泛型没有优势,那么只需接受接口类型的参数即可。使用泛型将迫使类型系统和JITter进行额外的工作,因此,如果没有好处,则不应该这样做。另一方面,通常至少会应用上述优点之一。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.