有哪些静态的鸭子类型语言?

20

我声明成员时能否指定接口?

思考这个问题一段时间后,我意识到静态鸭式语言实际上可能是可行的。为什么预定义类不能在编译时绑定到一个接口上呢?例如:

public interface IMyInterface
{
  public void MyMethod();
}

public class MyClass  //Does not explicitly implement IMyInterface
{
  public void MyMethod()  //But contains a compatible method definition
  {
    Console.WriteLine("Hello, world!");
  }
}

...

public void CallMyMethod(IMyInterface m)
{
  m.MyMethod();
}

...

MyClass obj = new MyClass();
CallMyMethod(obj);     // Automatically recognize that MyClass "fits" 
                       // MyInterface, and force a type-cast.

你知道有哪些编程语言支持这样的特性吗?在Java或C#中有用吗?它是否有根本性的缺陷?我知道你可以子类化MyClass并实现接口,或者使用适配器设计模式来实现相同的功能,但这些方法似乎只是不必要的样板代码。

15个回答

22

对于这个问题的全新答案,Go 语言正好拥有这个特性。我认为它非常的酷和聪明(虽然我会很感兴趣看看它在实际应用中的表现),同时也要称赞这一想法。

正如官方文档中所述(作为 Go 之旅的一部分,包含示例代码):

接口是隐式实现的

通过实现方法来实现接口类型。没有显式的声明意图,也没有 "implements" 关键字。

隐式接口将接口的定义与其实现解耦,因此可以在任何包中出现而无需预先安排。


你能否提供Go文档中解释这个工作原理的实际部分的链接?更好的是,能否在这里包含一个例子? - Paŭlo Ebermann
1
随你所愿(但只需链接到示例) - Grumdrig
1
如果有两个不同的接口具有相同的方法签名,会发生什么情况呢? - caramel1995

15

C++中使用模板怎么样?

class IMyInterface  // Inheritance from this is optional
{
public:
  virtual void MyMethod() = 0;
}

class MyClass  // Does not explicitly implement IMyInterface
{
public:
  void MyMethod()  // But contains a compatible method definition
  {
    std::cout << "Hello, world!" "\n";
  }
}

template<typename MyInterface>
void CallMyMethod(MyInterface& m)
{
  m.MyMethod();  // instantiation succeeds iff MyInterface has MyMethod
}

MyClass obj;
CallMyMethod(obj);     // Automatically generate code with MyClass as 
                       // MyInterface

我实际上还没有编译这段代码,但我认为它可以运行,并且是对原始建议(但不可用)代码的简单的 C++ 版本化。

1
有人提到了C++模板,但我认为那个答案被删除了。无论如何,模板基本上只是高级宏。对于您传递给它的每种不同类型,都会创建一个新的“CallMyMethod”,因此它并不真正进行类型推断。 - Cybis
2
公平地说,原帖并没有提到类型推断本身。我只是稍微调整了他的代码,并展示了它在C++中可以基本不变地工作。 - John Zwinck
5
我不明白为什么有人仍然认为C++模板只是高级宏,实际上它是一种图灵完备的编程机制,而非文本替换工具。请注意,我的翻译可能并不完美,但我已经尽力让它通顺易懂且保留原意。 - swegi
1
@swegi 真正的(语法)宏也是图灵完备的。而且更多。例如,在Clojure中,您可以在编译时完成几乎所有操作。 - Display Name
1
除了其他事情之外,C++模板甚至不是“高级”宏。它们在最好的情况下实际上只是非常基本的宏。任何需要自己编写if语句的语言(或元语言)都不能被认为是先进的编程环境。 - Alex Celeste
显示剩余2条评论

10

我不明白这样做的意义。为什么不明确表示该类实现了接口并完成它呢? 实现接口是告诉其他程序员,此类应按照接口所定义的方式行事的标志。只是在方法上使用相同的名称和签名,并不能保证设计者的意图是执行类似的操作。尽管可能如此,但为什么要留下解释和误用的空间呢?

你之所以可以在动态语言中成功地“摆脱”这种情况,更多是因为测试驱动开发(TDD)而不是语言本身。我认为,如果语言提供了向使用/查看代码的其他人提供这些指导的功能,那么应该使用它。它实际上改善了清晰度,并且值得多写几个字。如果你无法访问它,请使用适配器来明确声明接口与其他类的关系。


我同意,接口的目的就是描述某个东西是什么。无论何时都要使用它,这不是样板代码,而是良好的实践。 - Mark
2
为什么这种模式在动态语言中很成功,但在静态语言中被认为是不好的实践?TDD与动态类型有什么独特的关系?正如@mipadi所提到的,为什么面向对象的语言不能支持一种类似于Haskell风格的类型推断系统? - Cybis
从我有限的理解来看,Java泛型只是一种迂回的方式,实际上就是鸭子类型。 - James McMahon
@James:抱歉,不行。Java泛型相当于自动转换为/从Object类型的强制转换。您无法编写像上面显示的那样的代码。 - R. Martinho Fernandes
我不知道任何编程语言或框架可以识别“实现 IFoo 和 IBar 接口的对象集合”的概念,除非想要存储在集合中的所有内容都实现了继承自 IFoo 和 IBar 的接口 IFooBar。如果不存在通用类型,则代码只能将集合定义为容纳一个类型,并根据需要进行转换。从静态的角度来看,鸭子类型可以允许将集合类型定义为 {IFoo,IBar},并将其项视为同时满足两个约束条件。 - supercat
1
如果代码已经被编译,而且你没有源代码,因为它不是你的,那该怎么办呢?你如何让它继承一个接口?你不能总是期望别人做出正确的决定。 - symbiont

10

静态类型语言在编译时检查类型,而不是在运行时检查。以上系统的一个明显问题是编译器将在程序编译时检查类型,而不是在运行时检查。

现在,你可以在编译器中构建更多的智能功能,使其能够推导类型,而不是让程序员显式地声明类型; 编译器可能能够看到MyClass实现了一个MyMethod()方法,并相应地处理这种情况,无需显式声明接口(正如你所建议的那样)。这样的编译器可以利用类型推断,例如Hindley-Milner

当然,像Haskell这样的一些静态类型语言已经做到了类似于你所建议的事情; Haskell编译器能够推断类型(大部分情况下)而不需要显式声明它们。但是,显然Java/C#没有这个能力。


谢谢。我大约一周前开始学习Haskell,到目前为止它似乎很酷。不过,这是一个有点陡峭的学习曲线——以前从未涉足过函数式语言。无论如何,它的类型推断系统非常像我所提到的。 - Cybis
3
C#似乎具有类型推断。例如,这篇文章谈到了它。http://www.developer.com/net/csharp/article.php/3601646/Go-Inside-C-30s-Type-Inference-Process.htm - John K

9
F#支持静态鸭子类型,但有个限制:你必须使用成员约束。详细信息请参见此博客文章
引用自上述博客的示例:
let inline speak (a: ^a) =
    let x = (^a : (member speak: unit -> string) (a))
    printfn "It said: %s" x
    let y = (^a : (member talk: unit -> string) (a))
    printfn "Then it said %s" y

type duck() =
    member x.speak() = "quack"
    member x.talk() = "quackity quack"
type dog() =
    member x.speak() = "woof"
    member x.talk() = "arrrr"

let x = new duck()
let y = new dog()
speak x
speak y

6

TypeScript!

好吧,它是JavaScript的一个超集,可能并不构成一种“语言”,但这种静态鸭式类型在TypeScript中非常重要。

enter image description here


我认为Typescript使用结构类型(公平地说,这与鸭子类型非常相似,只是更严格)。 - frankelot
1
TS 实际上比这更进一步 - 不仅类隐式地分配给匹配的接口,而且类 本身 就是一个接口类型,这意味着你可以将其他类型分配给它们。使用此功能进行规模化构建非常简单- 在测试中替换依赖等等。轻松愉快。 - mindplay.dk

4
大多数ML语言支持具有推理和约束类型方案的结构类型,这是极客语言设计师术语,似乎最符合原始问题中"静态鸭子类型"一词的含义。
这个家族中较流行的语言包括:Haskell、Objective Caml、F#和Scala。当然,与您的示例最相似的是Objective Caml。以下是您示例的翻译:
open Printf

class type iMyInterface = object
  method myMethod: unit
end

class myClass = object
  method myMethod = printf "Hello, world!"
end

let callMyMethod: #iMyInterface -> unit = fun m -> m#myMethod

let myClass = new myClass

callMyMethod myClass

注意:有些名称需要更改以符合OCaml的标识符大小写语义,但除此之外,这是一个相当简单的翻译。
另外值得注意的是,在callMyMethod函数中的类型注释和iMyInterface类型的定义并不是严格必要的。Objective Caml可以在没有任何类型声明的情况下推断出你的示例中的所有内容。

3

Crystal 是一种静态类型语言,支持鸭子类型。 示例

def add(x, y)
  x + y
end

add(true, false)

add 调用导致了这个编译错误:

Error in foo.cr:6: instantiating 'add(Bool, Bool)'

add(true, false)
^~~

in foo.cr:2: undefined method '+' for Bool

  x + y
    ^

2

2

C++的新版本朝着静态鸭子类型的方向发展。你可以在某一天(今天?)编写如下内容:

auto plus(auto x, auto y){
    return x+y;
}

而且如果没有匹配的函数调用 x+y,编译将失败。

至于你的批评:

每次传递不同类型到 "CallMyMethod" 中都会创建一个新的 "CallMyMethod",因此它并不是真正的类型推断。

但这确实是类型推断(您可以说 foo(bar) 其中 foo 是一个模板化函数),并且具有相同的效果,只是更节省时间,在编译后的代码中占用更多空间。

否则,您将不得不在运行时查找方法。您必须查找名称,然后检查该名称是否具有具有正确参数的方法。

或者您将不得不存储所有与匹配接口相关的信息,并查看与接口匹配的每个类,然后自动添加该接口。

在任一情况下,这允许您隐式且意外地破坏类层次结构,这对于新功能来说是不好的,因为它违反了 C#/Java 程序员习惯。对于 C++ 模板,您已经知道自己处于一个雷区中(他们还添加了特性("概念")以允许对模板参数进行限制)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接