剥掉Scala的糖衣(11) -- structural types

Structural types,中文怎么翻译不确定。我们可以用它来实现类似于鸭子类型的效果。为什么说是“类似”鸭子类型呢?稍后会说到它和鸭子类型的区别。

举一个例子,看看它都可以做什么:

1
def makeNoise(quacker: {def quack(): String}) = quacker.quack

声明一个方法,叫做makeNoise,接受什么类型的参数呢?不做严格限制,我们只声明说参数必须有一个叫做quack的方法,该quack方法返回值类型为String。然后在makeNoise方法内调用quack方法。请注意我们并没有声明一个含有quack方法签名的接口或者类,我们仅仅是在声明参数的同时声明我们期待参数含有什么样的成员。

然后我们声明一个Duck类:

1
2
3
class Duck {
def quack() = "real quack"
}

这样就可以调用makeNoise方法了:

1
makeNoise(new Duck)

或者再声明一个NotADuck类:

1
2
3
class NotADuck {
def quack() = "fake quack"
}

也可以把它传给makeNoise方法:

1
makeNoise(new NotADuck)

甚至是匿名对象也可以:

1
2
3
makeNoise(new {
def quack() = "anonymous quack"
})

如果我们把Duck的quack方法改个名字:

1
2
3
class Duck {
def quackRenamed() = "real quack"
}

那么编译器就会对下面这行代码给出错误:

1
makeNoise(new Duck)

type mismatch, found : hello.Duck, required: AnyRef{def quack(): String}

是做了编译时类型检查的。

然后我们反编译代码,看看它是如何实现的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
private static Class[] reflParams$Cache1;
private static volatile SoftReference reflPoly$Cache1;

static {
Hello.reflParams$Cache1 = new Class[0];
Hello.reflPoly$Cache1 = new SoftReference((T)new EmptyMethodCache());
}

public static Method reflMethod$Method1(final Class x$1) {
MethodCache methodCache1 = Hello.reflPoly$Cache1.get();
if (methodCache1 == null) {
methodCache1 = (MethodCache)new EmptyMethodCache();
Hello.reflPoly$Cache1 = new SoftReference((T)methodCache1);
}
Method method1 = methodCache1.find(x$1);
if (method1 != null) {
return method1;
}
method1 = ScalaRunTime$.MODULE$.ensureAccessible(x$1.getMethod("quack", Hello.reflParams$Cache1));
Hello.reflPoly$Cache1 = new SoftReference((T)methodCache1.add(x$1, method1));
return method1;
}

public String makeNoise(final Object quacker) {
final Object invoke;
try {
invoke = reflMethod$Method1(quacker.getClass()).invoke(quacker, new Object[0]);
}
catch (InvocationTargetException ex) {
throw ex.getCause();
}
return (String)invoke;
}

我们可以看到,makeNoise方法的参数类型被编译成了Object。方法内部通过反射去调用quack方法。

再仔细看一下,方法内做了个catch,如果找不到quack方法就把异常抛出来。我们刚才不是看到有编译时类型检查吗?怎么会找不到quack方法呢?

其实找不到quack方法的情况还是会存在的。假如我们把以上代码打成jar包供别人调用,那别人看到的你这个方法要的是Object啊,随便传一个什么东西进来都可以。如果传入的参数没有quack方法,那自然就会有异常了。这也是一个很好的信号,告诉我们说这个语言特性不适合用在public API中。

刚开始时提到过,这个语言特性不能叫做鸭子类型,为什么呢?我们看两个真正鸭子类型的例子:

1
2
3
4
5
function makeNoise(quacker){
return quacker.quack()
}

makeNoise({quack:function(){return "quack quack"}})

上面的是JavaScript的,下面的是C#的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Duck
{
public String Quack ()
{
return "quack quack";
}
}

class Hello
{
public String MakeNoise (dynamic quacker)
{
return quacker.Quack ();
}
}

class MainClass
{
public static void Main (string[] args)
{
Console.Out.WriteLine (new Hello ().MakeNoise (new Duck ()));
}
}

在上面两段代码中,如果我们把quack或者Quack改个名字的话,都会导致运行失败。而且在声明参数的时候也没有指明我们期待有一个quack或者Quack成员的存在。所以说Scala的这个语言特性不能称之为鸭子类型的原因在于两点:

  • Scala做了编译时类型安全检查
  • 声明参数时显式的指明了期待的成员

存疑:虽然上面的C#例子看起来是鸭子类型,但是C#的wiki页面上的typing discipline一项里并没有列出duck typing。原因不明。

=======================================

后记:C#的wiki页面没有把duck typing列为其typing discipline的一项。但是,duck typing的wiki页面上又用C#举了例子。互相矛盾啊,于是我在stackoverflow上问了个问题,引来了C#编译器的程序员之一Eric Lippert,他回答问题之后还写了篇博客来表示对duck typing这个名词的困惑。看完之后我表示更晕了。

所以请不要把上面我说的关于duck typing的东西当真,随便瞄一眼就好了。duck typing是一个没有清晰定义的名词,在我们能够共同认同它的某一种特定的definition之前去讨论它是无谓的。

下面把链接列出:

C#的wiki (typing discipline在页面右侧的那个表里)

Duck typing的wiki

Eric Lippert的博客

以上链接,建议别点 :)