类型检查和鸭子类型 Duck typing in computer programming is an application of the duck test 鸭子测试 鸭子类型 指示编译器将类的类型检查安排在运行时而不是编译时 type checking can be specified to occur at run time rather than compile time.
阅读原文时间:2023年07月08日阅读:1

Go所提供的面向对象功能十分简洁,但却兼具了类型检查和鸭子类型两者的有点,这是何等优秀的设计啊!

Duck typing in computer programming is an application of the duck test 鸭子测试 鸭子类型 指示编译器将类的类型检查安排在运行时而不是编译时   type checking can be specified to occur at run time rather than compile time.

《代码的未来》

https://en.wikipedia.org/wiki/Duck_test

The duck test is a form of abductive reasoning. This is its usual expression:

If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck.

The test implies that a person can identify an unknown subject by observing that subject's habitual characteristics. It is sometimes used to counter abstruse arguments that something is not what it appears to be.

Indiana poet James Whitcomb Riley (1849–1916) may have coined the phrase when he wrote:

When I see a bird that walks like a duck and swims like a duck and quacks like a duck, I call that bird a duck.[1]

https://en.wikipedia.org/wiki/Duck_typing

Duck typing in computer programming is an application of the duck test—"If it walks like a duck and it quacks like a duck, then it must be a duck"—to determine if an object can be used for a particular purpose. With normal typing, suitability is determined by an object's type. In duck typing, an object's suitability is determined by the presence of certain methods and properties, rather than the type of the object itself.[1][2]

This is a simple example in Python 3 that demonstrates how any object may be used in any context, up until it is used in a way that it does not support.

class Duck:
def fly(self):
print("Duck flying")

class Sparrow:
def fly(self):
print("Sparrow flying")

class Whale:
def swim(self):
print("Whale swimming")

for animal in Duck(), Sparrow(), Whale():
animal.fly()

Output:

Duck flying
Sparrow flying
AttributeError: 'Whale' object has no attribute 'fly'

In some statically typed languages such as C# and Boo,[3][4] class type checking can be specified to occur at run time rather than compile time. Duck typing can be achieved in Java using the MethodHandle API.[5]

Structural type systems[edit]

Duck typing is similar to, but distinct from, structural typing. Structural typing is a static typing system that determines type compatibility and equivalence by a type's structure, whereas duck typing is dynamic and determines type compatibility by only that part of a type's structure that is accessed during run time.

The TypeScript,[6] OCamlScalaGoElm,[7] Gosu and PureScript languages support structural typing to varying degrees.

Protocols and interfaces[edit]

Protocols and interfaces may provide some of the benefits of duck typing, yet duck typing is distinct in not having an explicit interface defined. For example, if a third party library implements a class that cannot be modified, a client cannot use an instance of it with an interface unknown to that library even if the class does, in fact, satisfy the interface requirements. (A common solution to this problem is the Adapter pattern.) Duck typing would allow this. Again, all of an interface must be satisfied for compatibility.

Templates or generic types[edit]

Template, or generic functions or methods apply the duck test in a static typing context; this brings all the advantages and disadvantages of static versus dynamic type checking in general. Duck typing can also be more flexible in that only the methods actually called at runtime need to be implemented, while templates require implementations of all methods that can not be proven unreachable at compile time.

Languages like Python, Java and Objective-C are examples of duck typing because it is possible in them to construct new types in runtime via reflection and inspect whether these objects implement certain methods. On the other hand, there are languages that rely on compile-time metaprogramming techniques (like C++ and its template system) and thus do not fit into the category of duck typing; instead, at some point in the compilation pipeline, all placeholder types become substituted with some concrete types specified in a particular instantiation. Even though certain type erasure is possible in them, runtime inspection is limited.

Duck typing in computer programming is an application of the duck test

鸭子类型(英语:duck typing)在程序设计中是动态类型的一种风格。在这种风格中,一个对象有效的语义,不是由继承自特定的类或实现特定的接口,而是由"当前方法和属性的集合"决定。这个概念的名字来源于由詹姆斯·惠特科姆·莱利提出的鸭子测试(见下面的“历史”章节),“鸭子测试”可以这样表述:

“当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。”[1][2]

在鸭子类型中,关注点在于对象的行为,能作什么;而不是关注对象所属的类型。例如,在不使用鸭子类型的语言中,我们可以编写一个函数,它接受一个类型为"鸭子"的对象,并调用它的"走"和"叫"方法。在使用鸭子类型的语言中,这样的一个函数可以接受一个任意类型的对象,并调用它的"走"和"叫"方法。如果这些需要被调用的方法不存在,那么将引发一个运行时错误。任何拥有这样的正确的"走"和"叫"方法的对象都可被函数接受的这种行为引出了以上表述,这种决定类型的方式因此得名。

鸭子类型通常得益于"不"测试方法和函数中参数的类型,而是依赖文档、清晰的代码和测试来确保正确使用。

在常规类型中,我们能否在一个特定场景中使用某个对象取决于这个对象的类型,而在鸭子类型中,则取决于这个对象是否具有某种属性或者方法——即只要具备特定的属性或方法,能通过鸭子测试,就可以使用。

考虑用于一个使用鸭子类型的语言的以下伪代码

function calculate(a, b, c) => return (a+b)*c

example1 = calculate (1, 2, 3)
example2 = calculate ([1, 2, 3], [4, 5, 6], 2)
example3 = calculate ('apples ', 'and oranges, ', 3)

print to_string example1
print to_string example2
print to_string example3

在样例中,每次对calculate的调用都使用的对象(数字、列表和字符串)在继承关系中没有联系。只要对象支持“+”和“*”方法,操作就能成功。例如,翻译成RubyPython语言,运行结果应该是:

9
[1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6]
apples and oranges, apples and oranges, apples and oranges,

这样,鸭子类型在不使用继承的情况下使用了多态。唯一的要求是calculate函数需要作为参数的对象拥有“+”和“*”方法。以下样例(Python语言)体现了鸭子测试。就in_the_forest函数而言,对象是一个鸭子:

class Duck:
def quack(self):
print "这鸭子正在嘎嘎叫"

def feathers(self):  
    print "这鸭子拥有白色和灰色的羽毛"

class Person:
def quack(self):
print "这人正在模仿鸭子"

def feathers(self):  
    print "这人在地上拿起1根羽毛然后给其他人看"

def in_the_forest(duck):
duck.quack()
duck.feathers()

def game():
donald = Duck()
john = Person()
in_the_forest(donald)
in_the_forest(john)

game()

一些通常的静态语言如BooC#第四版,有一些额外的类型注解,它们指示编译器将类的类型检查安排在运行时而不是编译时,并在编译器的输出中包含用于运行时类型检查的代码[3][4]。这些附加的内容允许这些语言享受鸭子类型的大多数益处,仅有的缺点是需要在编译时识别和指定这些动态类。

结构类型系统[编辑]

鸭子类型和结构类型相似但与之不同。结构类型由类型的结构决定类型的兼容性和等价性,而鸭子类型只由结构中在运行时所访问的部分决定类型的兼容性。Objective Caml语言使用结构类型系统。

接口[编辑]

接口可以提供鸭子类型的一些益处,但鸭子类型与之不同的是没有显式定义任何接口。例如,如果一个第三方Java库实现了一个用户不允许修改的类,用户就无法把这个类的实例用作一个自己定义的接口的实现,而鸭子类型允许这样做。

模板或泛型[编辑]

模板函数或方法在一个静态类型上下文中应用鸭子测试;这同时带来了静态和动态类型检查的一般优点和缺点。同时,由于在鸭子类型中,只有“在运行时被实际调用的”方法需要被实现,而模板要求实现“在编译时不能证明不可到达的”所有方法,因此鸭子类型更具有可伸缩性。

实例包括带有模板的C++语言和Java语言的泛型。

关于鸭子类型常常被引用的一个批评是它要求程序员在任何时候都必须很好地理解他/她正在编写的代码。在一个强静态类型的、使用了类型继承树和参数类型检查的语言中,给一个类提供未预测的对象类型更为困难。例如,在Python中,你可以创建一个称为Wine的类,并在其中需要实现press方法。然而,一个称为Trousers的类可能也实现press()方法。为了避免奇怪的、难以检测的错误,开发者在使用鸭子类型时需要意识到每一个“press”方法的可能使用,即使在语义上和他/她所正在编写工作的代码没有任何关系。

本质上,问题是:“如果它走起来像鸭子并且叫起来像鸭子”,它也可以是一只正在模仿鸭子的龙。尽管它们可以模仿鸭子,但也许你不总是想让龙进入池塘。

鸭子类型的提倡者,如吉多·范罗苏姆,认为这个问题可以通过在测试和维护代码库前拥有足够的了解来解决[5][6]

对鸭子类型的批评倾向于成为关于动态类型和静态类型的争论的更广阔的观点的特殊情形。

Alex Martelli很早(2000年)就在发布到comp.lang.python新闻组上的一则消息 页面存档备份,存于互联网档案馆中使用了这一术语。他同时对鸭子测试的错误的字面理解提出了提醒,以避免人们错误认为这个术语已经被使用。

“换言之,不要检查它是不是一个鸭子:检查它像不像一个鸭子地,等等。取决于你需要哪个像鸭子的行为的子集来使用语言。”

在ColdFusion中[编辑]

web应用程序脚本语言ColdFusion允许函数参数被指定为类型为any。对于这种参数,任意对象都可被传入,函数调用在运行时被动态绑定。如果对象没有实现一个被调用的函数,一个可被捕获并优雅地处理的运行时异常将被抛出。在ColdFusion 8中,这也可以被一个已定义的事件onMissingMethod()而不是异常处理器处理。另一个可替代的参数类型WEB-INF.cftags.component限制传入参数是一个ColdFusion组件(CFC),在一个不正确的对象传入时它提供了更好的错误消息。

在C#中[编辑]

C# 4.0实现了动态成员查询(dynamic member lookup)实现了鸭子类型化。注意下例中类方法InTheForest的参数类型被声明为dynamic。

using System;

namespace DuckTyping
{
public class Duck
{
public void Quack() { Console.WriteLine("这鸭子正在嘎嘎叫"); }
public void Feathers() { Console.WriteLine("这鸭子拥有白色与灰色羽毛"); }
}

public class Person  
{  
    public void Quack()    { Console.WriteLine("这人正在模仿鸭子"); }  
    public void Feathers() { Console.WriteLine("这人在地上拿起1根羽毛然后给其他人看"); }  
}

internal class Program  
{  
    private static void InTheForest(dynamic duck)  
    {  
        duck.Quack();  
        duck.Feathers();  
    }

    private static void Game()  
    {  
        Duck donald = new Duck();  
        Person john = new Person();  
        InTheForest(donald);  
        InTheForest(john);  
    }

    private static void Main()  
    {  
        Game();  
    }  
}  

}

在Common Lisp中[编辑]

Common Lisp提供了一个面向对象的扩展(Common Lisp对象系统,简写为CLOS)。在Common Lisp中,CLOS和Lisp的动态类型使鸭子类型成为一种通用的编程风格。

使用Common Lisp,用户通常不需要查询类型,因为如果一个函数不适用,系统会抛出一个运行时错误。这个错误可以被Common Lisp的条件系统处理。在类外定义的方法也可以为指定的对象定义。

(defclass duck () ())

(defmethod quack ((a-duck duck))
(print "这鸭子正在嘎嘎叫"))

(defmethod feathers ((a-duck duck))
(print "这鸭子有白色和灰色羽毛"))

(defclass person () ())

(defmethod quack ((a-person person))
(print "这人正在模仿鸭子"))

(defmethod feathers ((a-person person))
(print "这人在地上拿起1根羽毛然后给其他人看"))

(defmethod in-the-forest (duck)
(quack duck)
(feathers duck))

(defmethod game ()
(let ((donald (make-instance 'duck))
(john (make-instance 'person)))
(in-the-forest donald)
(in-the-forest john)))

(game)

Common Lisp通常的开发风格(像SLIME一样使用Lisp REPL)也允许交互式修复:

? (defclass cat () ())
#
? (quack (make-instance 'cat))

Error: There is no applicable method for the generic function:
#
when called with arguments:
(#)
If continued: Try calling it again
1 > (defmethod quack ((a-cat cat))
(print "这猫正在模仿鸭子"))

#
1 > (continue)

"这猫正在模仿鸭子"

通过这种方法,软件可以通过扩展只有部分工作的使用鸭子类型的代码来开发。

在Objective-C中[编辑]

Objective-C,C和Smalltalk的一个交错,像Smalltalk一样,允许用户声明一个对象的类型为“id”并向它发送任何信息。发送者可以测试一个对象以了解它能不能对一个消息响应,对象可以在收到消息的时候决定响应与否,如果发送者发送了一个接收者不能响应的消息,一个异常会被抛出。因此,鸭子类型在Objective-C中被完全支持。

在Python中[编辑]

鸭子类型在Python中被广泛使用。Python术语表 页面存档备份,存于互联网档案馆这样定义鸭子类型:

Pythonic programming style that determines an object's type by inspection of its method or attribute signature rather than by explicit relationship to some type object ("If it looks like a duck and quacks like a duck, it must be a duck.") By emphasizing interfaces rather than specific types, well-designed code improves its flexibility by allowing polymorphic substitution. Duck-typing avoids tests using type() or isinstance(). Instead, it typically employs the EAFP (Easier to Ask Forgiveness than Permission) style of programming.

在Python中,鸭子类型的最典型例子就是类似file的类。这些类可以实现file的一些或全部方法,并可以用于file通常使用的地方。例如,GzipFile 页面存档备份,存于互联网档案馆实现了一个用于访问gzip压缩的数据的类似file的对象。cStringIO允许把一个Python字符串视作一个文件。套接字(socket)也和文件共同拥有许多相同的方法。然而套接字缺少tell()方法 页面存档备份,存于互联网档案馆,不能用于GzipFile可以使用的所有地方。这体现了鸭子类型的可伸缩性:一个类似file的对象可以实现它有能力实现的方法,且只能被用于它有意义的情形下。

EAFP原则描述了异常处理的使用。例如相对于检查一个自称为类似Duck的对象是否拥有一个quack()方法(使用if hasattr(mallard, "quack"): ...),人们通常更倾向于用异常处理把对quack的调用尝试包裹起来:

try:
mallard.quack()
except (AttributeError, TypeError):
print "mallard并沒有quack()函数"

这个写法的优势在于它鼓励结构化处理其他来自类的错误(这样的话,例如,一个不能完成quack的Duck子类可以抛出一个“QuackException”,这个异常可以简单地添加到包裹它的代码,并不需要影响更多的代码的逻辑。同时,对于其他不同类的对象存在不兼容的成员而造成的命名冲突,它也能够处理(例如,假设有一个医学专家Mallard有一个布尔属性将他分类为“quack=True”,试图执行Mallard.quack()将抛出一个TypeError)。

在更实际的实现类似file的行为的例子中,人们更倾向于使用Python的异常处理机制来处理各种各样的可能因为各种程序员无法控制的环境和operating system问题而发生的I/O错误。在这里,“鸭子类型”产生的异常可以在它们自己的子句中捕获,与操作系统、I/O和其他可能的错误分别处理,从而避开复杂的检测和错误检查逻辑。

github.com/sirupsen/logrus

logger.go:17

type Logger struct {
// The logs are `io.Copy`'d to this in a mutex. It's common to set this to a
// file, or leave it default which is `os.Stderr`. You can also set this to
// something more adventurous, such as logging to Kafka.
Out io.Writer

// 注意,Out类型为 io.Writer ,是一个接口, interface

src/io/io.go:90

// Writer is the interface that wraps the basic Write method.
//
// Write writes len(p) bytes from p to the underlying data stream.
// It returns the number of bytes written from p (0 <= n <= len(p))
// and any error encountered that caused the write to stop early.
// Write must return a non-nil error if it returns n < len(p).
// Write must not modify the slice data, even temporarily.
//
// Implementations must not retain p.
type Writer interface {
Write(p []byte) (n int, err error)
}

// 该 interface 只有一个方法,实现该方法 即可为 Logger的 Out

var log = logrus.New()

log.Out = os.Stdout
// 实现1-a

src/os/file.go:62

// Stdin, Stdout, and Stderr are open Files pointing to the standard input,
// standard output, and standard error file descriptors.
//
// Note that the Go runtime writes to standard error for panics and crashes;
// closing Stderr may cause those messages to go elsewhere, perhaps
// to a file opened later.
var (
Stdin = NewFile(uintptr(syscall.Stdin), "/dev/stdin")
Stdout = NewFile(uintptr(syscall.Stdout), "/dev/stdout")
Stderr = NewFile(uintptr(syscall.Stderr), "/dev/stderr")
)

src/os/file_unix.go:87

// NewFile returns a new File with the given file descriptor and
// name. The returned value will be nil if fd is not a valid file
// descriptor. On Unix systems, if the file descriptor is in
// non-blocking mode, NewFile will attempt to return a pollable File
// (one for which the SetDeadline methods work).
func NewFile(fd uintptr, name string) *File {
kind := kindNewFile
if nb, err := unix.IsNonblock(int(fd)); err == nil && nb {
kind = kindNonBlock
}
return newFile(fd, name, kind)
}

src/os/types.go:16
// File represents an open file descriptor.
type File struct {
*file // os specific
}

src/os/file_posix.go:47

// write writes len(b) bytes to the File.
// It returns the number of bytes written and an error, if any.
func (f *File) write(b []byte) (n int, err error) {
n, err = f.pfd.Write(b)
runtime.KeepAlive(f)
return n, err
}

// 实现1-b

internal/testutils/testutils.go:15
func LogAndAssertJSON(t *testing.T, log func(*Logger), assertions func(fields Fields)) {
var buffer bytes.Buffer
var fields Fields

logger := New()  
logger.Out = &buffer  
logger.Formatter = new(JSONFormatter)

log(logger)

err := json.Unmarshal(buffer.Bytes(), &fields)  
require.Nil(t, err)

assertions(fields)  

}

// 实现2-a

src/bytes/buffer.go:20

// Write appends the contents of p to the buffer, growing the buffer as
// needed. The return value n is the length of p; err is always nil. If the
// buffer becomes too large, Write will panic with ErrTooLarge.
func (b *Buffer) Write(p []byte) (n int, err error) {
b.lastRead = opInvalid
m, ok := b.tryGrowByReslice(len(p))
if !ok {
m = b.grow(len(p))
}
return copy(b.buf[m:], p), nil
}

// 实现2-b

手机扫一扫

移动阅读更方便

阿里云服务器
腾讯云服务器
七牛云服务器

你可能感兴趣的文章