聊一聊“鸭子类型“

2022-07-31,,

鸭子类型"也叫"鸭式辨型"等, 英文叫"Duck Typing”.

下面这句话不知道是谁说的, 只记得之前在读《Javascript权威指南》时遇到过

当看到一只动物走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只动物就可以被称为鸭子。

按我的话来说, 一个东西, 我不想管它是什么, 我只管它能做什么.

在程序语言中, 这种所谓的"鸭子类型"表现有两种形式: 动态和静态.

动态的例子:

def work(obj):
	obj.work()
	
class Programmer:
	def work(self):
		print("coding")
	
class Student:
	def work(self):
		print("learning")
	
if __name__ == '__main__':
	s = Student()
	p = Programmer()
	
	work(s)
	work(p)

代码开始定义的work(obj)函数是说: 你给我一个东西, 它只要能work()就行, 至于它是什么, 无所谓.
十分灵活.

但是这有个问题, 如果你传入一个没有work()方法的obj, 运行期间会报错, 比如:

# AttributeError: 'dict' object has no attribute 'work'
work({})

那么很明显, 如果我写了这个work函数, 别人随便传的话, 很容易出错, 这也是动态类型的缺点, 很难有效的限制参数的类型, 运行期容易出现类型方面的错误.
正所谓

动态类型一时爽,代码重构火葬场.

那么静态的怎么样呢?
以传统的C++, Java来说, 你必须实现相应的接口才可以, 不然编译都过不了, 这也就避免了一些因类型不对导致的问题:

// Obj.java
package com.test.service;

// 定义接口
public interface Obj {
	public void work();
}

// Student.java
package com.test.impl;
import com.test.service.Obj;
// Student实现了Obj接口, 所以可以work()
public class Student implements Obj {
	public void work() {
		System.out.println("learning");
	}
}

// FuErDie.java
package com.test.impl;
// FuErDie没有实现Obj接口, 所以不能work
public class FuErDie {
	public void play() {
		System.out.println("play...");
	}
}

// Application.java
package com.test.app;

import com.test.impl.FuErDie;
import com.test.impl.Student;
import com.test.service.Obj;

public class Application {
	public static void work(Obj obj) {
		obj.work();
	}
	public static void main(String[] args) {
		Student s = new Student();
		work(s); // 输出 learning
		
		FuErDie f = new FuErDie();
		// 报错: The method work(Obj) in the type Application is not applicable for the arguments (FuErDie)
		work(f);
	}
}

上面的例子很好地体现了静态语言的一些优势: 编译期间就可以检测到一些类型方面的错误.

而go语言的Duck Typing有点特别, 它结合了动态语言的灵活性,同时又会进行静态语言的类型检查,写起来是比较方便的。go 采用了折中的做法:不要求类型显示地声明实现了某个接口,只要实现了相关的方法即可,编译器就能检测到。

package main

import (
	"fmt"
)

type Work interface {
	work()
}

type Student struct {}

func (s *Student) work() {
	fmt.Println("learning")
}

func (s Student) String() string {
	return "不告诉你"
}

type FuErDie struct {}
func (f *FuErDie) play() {
	fmt.Println("play...")
}

func work(obj Work) {
	obj.work()
}

func main() {
	s := &Student{}
	work(s)

	f := &FuErDie{}
	// ./main.go:35:6: cannot use f (type *FuErDie) as type Work in argument to work:
	//        *FuErDie does not implement Work (missing work method)
	work(f)

	fmt.Println(s)
}

可以看到, 在go中实现一个接口, 不需要显示地写"implement xxx", 只要实现其中的方法就行.
而且接口与其具体的实现之间几乎没有耦合.
所以在go中, 你可能无意间实现了很多接口.

欢迎补充指正!

(完)

本文地址:https://blog.csdn.net/butterfly5211314/article/details/107591816

《聊一聊“鸭子类型“.doc》

下载本文的Word格式文档,以方便收藏与打印。