我已经使用python几天了,我想我了解动态和静态类型之间的区别。我不了解的是在什么情况下会首选。它是灵活且易读的,但是要付出更多的运行时检查和其他必需的单元测试的代价。
除了灵活性和可读性之类的非功能性标准之外,还有什么理由选择动态类型?使用动态类型我该怎么办呢?您能想到什么特定的代码示例来说明动态类型的具体优势?
我已经使用python几天了,我想我了解动态和静态类型之间的区别。我不了解的是在什么情况下会首选。它是灵活且易读的,但是要付出更多的运行时检查和其他必需的单元测试的代价。
除了灵活性和可读性之类的非功能性标准之外,还有什么理由选择动态类型?使用动态类型我该怎么办呢?您能想到什么特定的代码示例来说明动态类型的具体优势?
Answers:
既然您要举一个具体的例子,我就举一个例子。
Rob Conery的Massive ORM是400行代码。之所以这么小,是因为Rob能够映射SQL表并提供对象结果,而无需大量静态类型来镜像SQL表。这是通过使用dynamic
C#中的数据类型来完成的。Rob的网页详细描述了此过程,但是很明显,在这种特定的用例中,动态键入在很大程度上负责代码的简洁性。
与Sam Saffron的Dapper相比,后者使用静态类型。的SQLMapper
类单独是3000行的代码。
请注意,通常的免责声明适用,您的里程可能会有所不同;Dapper与Massive有不同的目标。我仅以此为例,说明您可以在400行代码中完成的工作,而如果没有动态类型,则可能无法实现。
动态类型使您可以将类型决定推迟到运行时。 就这样。
无论您使用动态类型的语言还是静态类型的语言,您的类型选择都必须仍然明智。除非字符串中包含数字数据,否则您不会将两个字符串加在一起并期望得到数字答案;如果字符串中不包含数字数据,那么您将得到意想不到的结果。静态类型的语言一开始就不允许您这样做。
支持静态类型语言的人指出,编译器可以在执行单行之前在编译时对代码进行大量的“合理性检查”。这是一件好事。
C#具有dynamic
关键字,它使您可以将类型决策推迟到运行时,而不会在其余代码中失去静态类型安全的好处。类型推断(var
)消除了始终显式声明类型的需要,从而消除了用静态类型语言编写代码的许多麻烦。
动态语言似乎更喜欢一种更具交互性的即时编程方法。没有人期望您必须编写一个类并经历一个编译周期才能键入一些Lisp代码并看着它执行。但这正是我期望在C#中所做的。
诸如“静态类型”和“动态类型”之类的短语到处都是,人们倾向于使用微妙的不同定义,所以让我们从澄清我们的意思开始。
考虑一种具有静态类型的语言,该类型在编译时进行检查。但是要说类型错误仅生成非致命的警告,并且在运行时,所有内容都是鸭子类型的。这些静态类型仅是为了程序员的方便,并不影响代码生成。这说明静态类型本身并不施加任何限制,并且与动态类型不互斥。(Objective-C很像这样。)
但是大多数静态类型的系统不会以这种方式运行。静态类型系统有两个常见的属性,可能会施加一些限制:
这是一个限制,因为许多类型安全程序必然包含静态类型错误。
例如,我有一个需要同时作为Python 2和Python 3运行的Python脚本。某些函数在Python 2和3之间更改了其参数类型,因此我具有如下代码:
if sys.version_info[0] == 2:
wfile.write(txt)
else:
wfile.write(bytes(txt, 'utf-8'))
Python 2静态类型检查器将拒绝Python 3代码(反之亦然),即使它永远不会执行。我的类型安全程序包含静态类型错误。
再举一个例子,考虑一个要在OS X 10.6上运行但要利用10.7的新功能的Mac程序。10.7方法在运行时可能存在或不存在,并且由我(程序员)来检测它们。静态类型检查器将不得不拒绝我的程序以确保类型安全,或者被接受该程序,并可能在运行时产生类型错误(函数丢失)。
静态类型检查假定编译时信息充分描述了运行时环境。但是预测未来是危险的!
这里还有一个限制:
假定静态类型为“正确”可提供许多优化机会,但这些优化可能会受到限制。一个很好的例子是代理对象,例如远程处理。假设您希望有一个本地代理对象,该对象将方法调用转发到另一个进程中的实际对象。如果代理是通用的(这样它就可以伪装成任何对象)并且是透明的(这样,现有的代码就不必知道它正在与代理进行对话),那就太好了。但是为此,编译器无法生成假定静态类型正确的代码(例如通过静态内联方法调用),因为如果对象实际上是代理,则该操作将失败。
这种远程操作的示例包括ObjC的NSXPCConnection或C#的TransparentProxy(其实现需要在运行时中进行一些简化,请参见此处的讨论)。
当代码生成不依赖于静态类型,并且您拥有消息转发之类的功能时,您可以使用代理对象,调试等做很多有趣的事情。
因此,这是一些示例的样本,如果您不需要满足类型检查器的要求。这些限制不是由静态类型强加的,而是由强制执行的静态类型检查强加的。
A Python 2 static type checker would reject the Python 3 code (and vice versa), even though it would never be executed. My type safe program contains a static type error.
在任何合理的静态语言中,都可以使用IFDEF
类型预处理器语句来执行此操作,同时在两种情况下都保持类型安全。
鸭子类型的变量是每个人都想到的第一件事,但是在大多数情况下,您可以通过静态类型推断获得相同的好处。
但是,以任何其他方式很难实现在动态创建的集合中键入鸭子:
>>> d = JSON.parse(foo)
>>> d['bar'][3]
12
>>> d['baz']['qux']
'quux'
那么,JSON.parse
返回什么类型呢?整数或字典字符串数组的字典?不,即使这样还不够普遍。
JSON.parse
必须返回某种“变量值”,该变量可以递归地为null,bool,float,string,这些类型中的任何一种的数组,或者从string递归返回这些类型中的任何一种的字典。动态类型化的主要优点来自于具有此类变体类型。
到目前为止,这是动态类型而不是动态类型语言的好处。体面的静态语言可以完美地模拟任何此类类型。(甚至“不好的”语言也常常可以通过在后台破坏类型安全性和/或要求笨拙的访问语法来模拟它们。)
动态类型语言的优点是静态类型推断系统无法推断此类类型。您必须显式地编写类型。但是,在许多此类情况下(包括一次),描述类型的代码与在不描述类型的情况下解析/构造对象的代码一样复杂,因此仍然不一定具有优势。
由于与它所关注的编程语言相比,每个远程实际使用的静态类型系统都受到严格限制,因此它无法表示可以在运行时检查的所有不变量。为了不避开类型系统试图给出的保证,因此选择保守并禁止使用情况,这些情况将通过这些检查,但不能(在类型系统中)被证明是通过的。
我举一个例子。假设您实现了一个简单的数据模型来描述数据对象,它们的集合等,该模型是静态类型的,也就是说,如果模型说x
类型为Foo的对象的属性包含一个整数,则它必须始终包含一个整数。因为这是一个运行时构造,所以不能静态键入它。假设您存储YAML文件中描述的数据。您创建一个哈希映射(稍后将传递给YAML库),获取x
属性,将其存储在映射中,获取恰好是字符串的其他属性,...等一下?the_map[some_key]
现在是什么类型?好吧,我们知道some_key
是'x'
,因此结果必须是整数,但是类型系统甚至无法开始对此进行推理。
一些经过积极研究的类型系统可能适用于此特定示例,但是它们非常复杂(包括编译器作者的实现和程序员的推理能力),尤其是对于这种“简单”的东西(我是说,我只是在一个例子中进行了解释)段)。
当然,今天的解决方案是将所有内容装箱,然后进行强制转换(或使用一堆被覆盖的方法,其中大多数方法会引发“未实现”异常)。但这不是静态类型,它是围绕类型系统的一种技巧,可以在运行时进行类型检查。
使用动态类型不能执行与使用静态类型不能执行的任何操作,因为可以在静态类型的语言之上实现动态键入。
Haskell中的一个简短示例:
data Data = DString String | DInt Int | DDouble Double
-- defining a '+' operator here, with explicit promotion behavior
DString a + DString b = DString (a ++ b)
DString a + DInt b = DString (a ++ show b)
DString a + DDouble b = DString (a ++ show b)
DInt a + DString b = DString (show a ++ b)
DInt a + DInt b = DInt (a + b)
DInt a + DDouble b = DDouble (fromIntegral a + b)
DDouble a + DString b = DString (show a ++ b)
DDouble a + DInt b = DDouble (a + fromIntegral b)
DDouble a + DDouble b = DDouble (a + b)
在足够的情况下,您可以实现任何给定的动态类型系统。
相反,您也可以将任何静态类型的程序转换为等效的动态程序。当然,您将失去静态类型语言提供的所有编译时对正确性的保证。
编辑:我想保持这个简单,但是这里是有关对象模型的更多详细信息
函数将Data列表作为参数,并在ImplMonad中执行具有副作用的计算,然后返回Data。
type Function = [Data] -> ImplMonad Data
DMember
是成员值或函数。
data DMember = DMemValue Data | DMemFunction Function
扩展Data
以包括对象和功能。对象是命名成员的列表。
data Data = .... | DObject [(String, DMember)] | DFunction Function
这些静态类型足以实现我熟悉的每个动态类型的对象系统。
Data
。
+
运算符,该运算符将两个Data
值合并为另一个Data
值。 Data
代表动态类型系统中的标准值。
膜:
膜是围绕整个对象图的包装,而不是仅用于单个对象的包装。通常,膜的创建者开始只是将单个对象包裹在膜中。关键思想是,穿过膜的任何对象参照物本身都将被可传递地包裹在同一膜中。
每种类型都由具有相同接口的类型包装,但是它会截取消息并在跨膜时包装和拆开值。您最喜欢的静态类型化语言中的wrap函数的类型是什么?也许Haskell具有该功能的类型,但是大多数静态类型的语言却没有,或者最终使用Object→Object,从而有效地放弃了其作为类型检查器的责任。
class Foo a where ...
data Wrapper = forall a. Foo a => Wrapper a
String
因为它是Java中的一种具体类型。Smalltalk不会出现此问题,因为它不会尝试键入#doesNotUnderstand
。
就像有人提到的那样,如果您自己实现某些机制,那么从理论上讲,与动态类型相比,您无能为力。大多数语言都提供类型松弛机制来支持类型灵活性,例如空指针,根对象类型或空接口。
更好的问题是,为什么在某些情况和问题下动态类型更合适,更合适。
首先,让我们定义
实体 -我需要代码中一些实体的一般概念。它可以是任何类型,从原始数到复杂数据。
行为 -可以说我们的实体具有某种状态和一组方法,这些方法和方法可以让外界指示实体做出某些反应。让我们将此实体的状态+接口称为行为。一个实体可以具有工具语言提供的以某种方式组合的多个行为。
实体及其行为的定义 -每种语言都提供了某种抽象方法,可帮助您定义程序中某些实体的行为(方法集+内部状态)。您可以为这些行为指定一个名称,并说所有具有此行为的实例都是特定类型的。
这可能不是那么陌生。正如您所说,您理解了差异,但仍然如此。可能还不完整和最准确的解释,但我希望足够有趣以带来一些价值:)
静态类型化 -在开始运行代码之前,在编译时检查程序中所有实体的行为。这意味着,例如,如果您想要类型为Person的实体具有行为(行为类似)Magician,则必须定义实体MagicianPerson并赋予其类似throwMagic()的魔术师行为。如果您在代码中,错误地告诉普通的Person.throwMagic()编译器会告诉您"Error >>> hell, this Person has no this behavior, dunno throwing magics, no run!".
动态类型化 -在动态类型化环境中,直到您真正尝试对某些实体进行操作之前,才检查实体的可用行为。在您的代码真正出现之前,不会询问运行Person.throwMagic()的Ruby代码。这听起来令人沮丧,不是吗。但这听起来也具有启发性。基于此属性,您可以做一些有趣的事情。举例来说,假设您设计了一款游戏,万物都能转向魔术师,而您真正不知道会是谁,直到您到达代码中的特定点为止。然后青蛙来,你说HeyYouConcreteInstanceOfFrog.include Magic
从那时起,这只青蛙就变成了一只具有魔力的特殊青蛙。其他青蛙,仍然没有。您会看到,在静态类型化语言中,您将必须通过某种标准的行为组合(例如接口实现)来定义此关系。在动态类型化语言中,您可以在运行时执行此操作,因此不会有人在意。
大多数动态类型化语言都具有提供通用行为的机制,该行为将捕获传递到其接口的任何消息。如果我还记得的话,例如Ruby method_missing
和PHP __call
。这意味着您可以在程序运行时做任何有趣的事情,并根据当前程序状态做出类型决定。这就带来了比传统的静态编程语言(如Java)灵活得多的建模工具。