我已经使用python几天了,我想我了解动态和静态类型之间的区别。我不了解的是在什么情况下会首选。它是灵活且易读的,但是要付出更多的运行时检查和额外的必需的单元测试的代价。
除了诸如灵活性和可读性之类的非功能性标准之外,还有什么理由选择动态类型?我该如何处理动态类型,否则无法实现?您能想到什么具体的代码示例来说明动态类型化的具体优势?
#1 楼
既然您要举一个具体的例子,我就举一个例子。Rob Conery的大规模ORM是400行代码。之所以这么小,是因为Rob能够映射SQL表并提供对象结果,而无需大量静态类型来镜像SQL表。这是通过使用C#中的
dynamic
数据类型来完成的。罗布(Rob)的网页详细描述了此过程,但是显然,在这种特殊的用例中,动态键入在很大程度上负责了代码的简洁性。使用静态类型;仅SQLMapper
类就是3000行代码。 请注意,通常的免责声明适用,您的里程可能会有所不同; Dapper与Massive有不同的目标。我仅以一个示例为例,您可以在400行代码中完成某些操作,而如果没有动态键入,则可能无法实现。运行时的决定。就这样。
无论您使用动态类型的语言还是静态类型的语言,您的类型选择都必须仍然明智。除非字符串中包含数字数据,否则您将不会将两个字符串加在一起并期望得到数字答案;如果字符串中不包含数字数据,则将得到意外的结果。静态类型的语言一开始就不允许您这样做。
静态类型语言的支持者指出,编译器可以在编译时对代码进行大量的“健全性检查”,单行执行。这是一个好东西。C#具有
dynamic
关键字,它使您可以将类型决策推迟到运行时,而不会在其余代码中失去静态类型安全的好处。类型推断(var
)消除了始终显式声明类型的需要,从而消除了用静态类型语言编写代码时的许多麻烦。动态语言似乎确实倾向于一种更具交互性的即时编程方法。没有人期望您必须编写一个类并经历一个编译周期才能键入一些Lisp代码并看着它执行。但这正是我期望在C#中执行的操作。评论
如果将两个数字字符串加在一起,我仍然不会期望得到数字结果。
– pdr
2012年10月3日在20:40
@Robert我同意你的大部分回答。但是,请注意,存在具有交互式read-eval-print循环的静态类型的语言,例如Scala和Haskell。 C#可能不是一种特别的交互式语言。
– Andres F.
2012年10月3日20:57
要学会给我一个Haskell。
–罗伯特·哈维(Robert Harvey)
2012年10月3日,21:25
@RobertHarvey:如果您还没有尝试过,可能会对F#感到惊讶/印象深刻。您将获得通常使用.NET语言获得的所有(编译时)类型安全性,只是很少需要声明任何类型。 F#中的类型推断超出了C#中的可用/有效范围。另外:类似于Andres和Daniel指出的那样,F#交互是Visual Studio的一部分...
–史蒂文·埃弗斯(Steven Evers)
2012年10月3日在21:40
“抱歉,除非字符串包含数字数据,否则您将不会将两个字符串加在一起,并且期望得到一个数字答案;否则,您将获得意想不到的结果。”抱歉,这与动态和静态类型无关,这是强类型还是弱类型。
–vartec
2012年10月5日12:50
#2 楼
诸如“静态类型”和“动态类型”之类的短语到处都是,人们倾向于使用微妙的不同定义,所以让我们从阐明我们的意思开始。考虑具有静态类型的语言在编译时检查。但是要说类型错误仅生成非致命的警告,并且在运行时,所有内容都是鸭子类型的。这些静态类型仅是为了程序员的方便,并不影响代码生成。这说明静态类型本身并不施加任何限制,并且与动态类型不互斥。 (Objective-C很像这样。)
但是大多数静态类型系统的行为都不是这种方式。静态类型系统有两个常见属性,可能会施加限制:
编译器可能会拒绝包含静态类型错误的程序。
这是一个限制,因为许多类型安全程序必然包含静态类型错误。
例如,我有一个Python脚本,需要同时作为Python 2和Python 3运行。某些函数在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方法在运行时可能存在也可能不存在,由我(程序员)来检测它们。静态类型检查器被迫拒绝我的程序以确保类型安全或接受程序,并可能在运行时产生类型错误(函数丢失)。
静态类型检查假定编译时间信息充分描述了运行时环境。但是预测未来是危险的!
这里还有一个局限性:
编译器可能会生成假定运行时类型为静态类型的代码。
假定静态类型“正确”可提供许多优化机会,但是这些优化可能会受到限制。代理对象就是一个很好的例子,例如远程处理。假设您希望有一个本地代理对象,该对象将方法调用转发到另一个进程中的实际对象。如果代理是通用的(这样它就可以伪装成任何对象)并且透明(这样,现有的代码就不必知道它正在与代理进行对话),那就太好了。但是要这样做,编译器无法生成假定静态类型正确的代码,例如通过静态内联方法调用,因为如果对象实际上是代理,则调用将失败。
当代码源不依赖于静态类型,并且具有消息转发之类的功能时,您可以使用代理对象,调试等做很多很棒的事情。 />
因此,这是一些示例的样本,如果您不需要满足类型检查器的要求,可以这样做。这些限制不是由静态类型强加的,而是由强制执行的静态类型检查强加的。
评论
“ Python 2静态类型检查器将拒绝Python 3代码(反之亦然),即使它永远不会执行。我的类型安全程序包含静态类型错误。”听起来您真正需要的是某种“静态if”,如果条件为false,则编译器/解释器甚至看不到代码。
–大卫·斯通(David Stone)
2012年10月26日下午2:09
@davidstone在c ++中存在
–Milind R
2014年12月1日18:19
Python 2静态类型检查器将拒绝Python 3代码(反之亦然),即使它永远不会执行。我的类型安全程序包含一个静态类型错误。在任何合理的静态语言中,都可以使用IFDEF类型的预处理程序语句来执行此操作,同时在两种情况下都保持类型安全。
–梅森·惠勒
2014年12月8日15:12
@ MasonWheeler,Davidstone不,预处理器技巧和static_if都太静态了。在我的示例中,我使用了Python2和Python3,但就像AmazingModule2.0和AmazingModule3.0一样,它们之间的接口在版本之间发生了变化,这很容易。您最早可以知道该接口是在模块导入时,这一定是在运行时(至少在您希望支持动态链接时)。
–ridiculous_fish
2014年12月13日12:30
#3 楼
鸭子类型的变量是每个人都想到的第一件事,但是在大多数情况下,可以通过静态类型推断获得相同的好处。方式:>>> d = JSON.parse(foo)
>>> d['bar'][3]
12
>>> d['baz']['qux']
'quux'
那么,
JSON.parse
返回什么类型?整数或字典字符串数组的字典?不,即使这样还不够通用。从字符串递归到任何这些类型。动态类型的主要优点来自于具有此类的变体类型。到目前为止,这是动态类型的好处,而不是动态类型的语言的好处。体面的静态语言可以完美地模拟任何此类类型。 (甚至“不好的”语言也常常可以通过在后台破坏类型安全性和/或要求笨拙的访问语法来模拟它们。)静态类型推断系统。您必须显式地编写类型。但是在许多此类情况下(包括一次),描述类型的代码与在不描述类型的情况下解析/构造对象的代码一样复杂,因此仍然不一定具有优势。评论
您的JSON解析示例可以轻松地由代数数据类型静态处理。
–user39685
2012年10月3日在20:25
好吧,我的答案还不够清楚。谢谢。 JSValue是动态类型的显式定义,正好是我在说的。有用的是那些动态类型,而不是需要动态类型的语言。但是,仍然重要的是,动态类型不能由任何实型推断系统自动生成,而大多数常见的例子都是微不足道的。我希望新版本能更好地解释它。
–abarnert
2012年10月3日20:37
@MattFenwick代数数据类型在实践中几乎仅限于功能语言。 Java和c#之类的语言呢?
–spirc
2012年10月5日,下午6:25
ADT在C / C ++中以标记的联合形式存在。这并非功能语言所独有。
–克拉克·盖贝尔(Clark Gaebel)
2012年10月5日13:39
@spirc您可以使用多个类来模拟经典的OO语言中的ADT,这些类都从一个公共接口派生,对getClass()或GetType()的运行时调用以及相等性检查。或者您可以使用双重调度,但是我认为这在C ++中会带来更多收益。因此,您可能具有JSObject接口以及JSString,JSNumber,JSHash和JSArray类。然后,您将需要一些代码来将该“未类型化”的数据结构转换为“应用程序类型”的数据结构。但是您可能也想使用动态类型的语言来执行此操作。
–丹尼尔·扬科斯基(Daniel Yankowsky)
2012年10月9日在1:42
#4 楼
由于与它所关注的编程语言相比,每个远程实际使用的静态类型系统都受到严格限制,因此它无法表示可以在运行时检查的所有不变量。为了不避开类型系统试图提供的保证,因此选择保守并禁止使用案例,这些用例将通过这些检查,但不能(在类型系统中)得到证明。我举一个例子。假设您实现了一个简单的数据模型来描述数据对象,它们的集合等,该模型是静态类型的,也就是说,如果模型说类型Foo的对象的属性
x
包含整数,则它必须始终包含整数。因为这是一个运行时构造,所以不能静态键入它。假设您存储YAML文件中描述的数据。您创建一个哈希映射(稍后将传递给YAML库),获取x
属性,将其存储在映射中,获取恰好是字符串的其他属性,...等一下? the_map[some_key]
现在是什么类型?好吧,我们知道some_key
是'x'
,因此结果必须是整数,但是类型系统甚至无法开始对此进行推理。一些积极研究的类型系统可能对此适用例如,但是它们极其复杂(既要由编译器作者实现,又要让程序员进行推理),尤其是对于这种“简单”的东西(我是说,我只是在一个段落中进行了解释)。
当然,今天的解决方案是将所有内容装箱,然后进行强制转换(或使用一堆被覆盖的方法,其中大多数方法会引发“未实现”异常)。但这不是静态类型,它是围绕类型系统的一种技巧,可以在运行时进行类型检查。
评论
泛型类型没有装箱要求。
–罗伯特·哈维(Robert Harvey)
2012年10月3日20:47
@RobertHarvey是的。我不是在谈论Java C#中的拳击,而是在谈论“将其包装在某些包装器类中,其唯一目的是代表U的子类型中的T值”。参数多态性(您称为通用类型)不适用于我的示例。这是对具体类型的编译时抽象,但是我们需要一种运行时键入机制。
–user7043
2012年10月3日20:49
值得指出的是,Scala的类型系统是图灵完整的。因此,类型系统可能比您想象的要简单。
– Andrea
2012年10月3日,21:11
@Andrea我故意没有将我的描述简化为完整。曾经在巡回演出中编程过吗?还是试图将这些东西编码为类型?在某些时候,它变得太复杂以至于不可行。
–user7043
2012年10月4日,12:59
@delnan我同意。我只是指出类型系统可以完成相当复杂的工作。我的印象是,您的答案意味着类型系统只能进行简单的验证,但是在第二遍阅读中,您没有写任何类似的东西!
– Andrea
2012年10月4日14:12
#5 楼
动态类型与静态类型没有任何关系,因为您可以在静态类型的语言之上实现动态类型。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)
在足够多的情况下,您可以实现任何给定的动态类型系统。
相反,您也可以将任何静态类型的程序转换为等效的动态程序。当然,您将失去静态类型语言提供的所有编译时对正确性的保证。
编辑:我想保持简单,但是这里有关于对象模型的更多详细信息
函数将数据列表作为参数并在ImplMonad中执行具有副作用的计算,然后返回数据。
type Function = [Data] -> ImplMonad Data
DMember
是成员值或函数。data DMember = DMemValue Data | DMemFunction Function
扩展
Data
以包含对象和函数。对象是命名成员的列表。data Data = .... | DObject [(String, DMember)] | DFunction Function
这些静态类型足以实现我熟悉的每个动态类型的对象系统。
评论
这根本不一样,因为您不能在不重新访问Data的定义的情况下添加新类型。
–杰德
2012年10月5日12:53
您将示例中的动态类型化和弱类型混合在一起。动态类型是关于对未知类型进行操作,而不是定义允许的类型列表以及这些类型之间的重载操作。
–小腿
2012年10月5日14:52
@Jed一旦实现了对象模型,基本类型和原始操作,就不需要其他基础工作了。您可以轻松,自动地将原始动态语言的程序翻译成该方言。
–NovaDenizen
2012年10月5日15:30
@hcalves由于您是在Haskell代码中提到重载,因此我怀疑您对它的语义不太了解。我定义了一个新的+运算符,该运算符将两个Data值组合为另一个Data值。数据表示动态类型系统中的标准值。
–NovaDenizen
2012年10月5日15:35
@Jed:大多数动态语言都有少量的“原始”类型和一些引入新值(数据结构,如列表)的归纳方式。例如,Scheme远远超出了原子,对和向量。您应该能够以与给定动态类型的其余部分相同的方式来实现它们。
– Tikhon Jelvis
2012年10月10日16:19
#6 楼
膜:膜是整个对象图的包装,而不是单个对象的包装。通常,膜的创建者开始只是将单个对象包裹在膜中。关键思想是,任何穿过该膜的对象引用本身都将被可传递地包裹在同一膜中。消息,并在它们穿过膜时包装和解开值。您最喜欢的静态类型化语言中的wrap函数的类型是什么? Haskell可能具有该功能的类型,但是大多数静态类型的语言没有,或者最终使用Object→Object,从而有效地放弃了其作为类型检查器的责任。
评论
是的,Haskell确实可以使用存在性类型来实现这一点。如果您有一些类型类Foo,则可以对实例化该接口的任何类型进行包装。 Foo类,其中...数据包装器=全部a。 Foo a =>包装器a
–杰克·麦克阿瑟(Jake McArthur)
2012年10月5日15:22
@JakeMcArthur,感谢您的解释。那是我坐下来学习Haskell的另一个原因。
–迈克·塞缪尔(Mike Samuel)
2012年10月5日18:27
您的膜是一个“接口”,并且对象的类型是“现有类型”的-也就是说,我们知道它们存在于该接口下,但是仅此而已。自80年代以来就已经知道数据抽象的现有类型。一个很好的参考书是cs.cmu.edu/~rwh/plbook/book.pdf第21.1章
–唐·斯图尔特
2012年10月5日19:27
@DonStewart。那么Java代理类是存在类型机制吗?膜变得困难的地方之一是使用标称类型系统的语言,这些系统在该类型的定义之外可以看到具体类型的名称。例如,一个人不能包装String,因为它是Java中的一种具体类型。 Smalltalk不会出现此问题,因为它不会尝试键入#doesNotUnderstand。
–迈克·塞缪尔(Mike Samuel)
2012年10月6日,0:39
#7 楼
就像有人提到的那样,如果您自己实现某些机制,那么从理论上讲,与动态类型相比,您无能为力。大多数语言都提供类型松弛机制来支持类型灵活性,例如空指针,根对象类型或空接口。首先,让我们定义
实体-我需要代码中某个实体的一般概念。从原始数到复杂数据,它都可以是任何东西。让我们将此实体的状态+接口称为行为。一个工具实体可以通过工具语言提供的某种方式以某种方式组合多个行为。方法+内部状态)。您可以为这些行为指定一个名称,并说所有具有此行为的实例都具有某种类型。
这可能并不陌生。正如您所说,您理解了差异,但仍然如此。可能不完整,最准确的解释,但我希望足够有趣以带来一些价值:)
静态类型化-在开始运行代码之前,在编译时检查程序中所有实体的行为。这意味着,例如,如果您想要“人”类型的实体具有行为(行为类似)魔术师,那么您将必须定义实体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)灵活得多的建模工具。
评论
从理论上讲,只要这两种语言都是图灵完备的,您什么都做不到。对我而言,更有趣的问题是,一个与另一个相对容易或自然。我经常在Python中做一些事情,即使我知道它有能力,我什至不会在C ++中考虑。正如克里斯·史密斯(Chris Smith)在其出色的论文中所写,在辩论类型系统之前要了解的内容:“在这种情况下,问题是大多数程序员经验有限,并且没有尝试过多种语言。就上下文而言,这里有六到七种不会算作“很多”。...这有两个有趣的结果:(1)许多程序员使用非常差的静态类型语言。(2)许多程序员使用非常差的动态类型语言。 >
@suslik:如果语言原语具有荒谬的类型,那么您当然可以对类型进行荒谬的操作。这与静态和动态类型之间的差异无关。
@CzarekTomczak:是的,这是某些动态类型语言的功能。但是静态类型的语言可能会在运行时被修改。例如,Visual Studio允许您在调试器中处于断点时重写C#代码,甚至倒带指令指针以使用新更改重新运行代码。正如我在另一条评论中引用克里斯·史密斯(Chris Smith)的话:“许多程序员使用了非常差的静态类型语言” —不要用您所知道的语言来判断所有静态类型语言。
@WarrenP:您断言“动态类型系统减少了我必须输入的多余内容”,但随后将Python与C ++进行了比较。那不是一个公平的比较:当然,C ++比Python更冗长,但这不是因为它们的类型系统不同,而是因为它们的语法不同。如果只想减少程序源中的字符数,请学习J或APL:我保证它们会更短。一个更公平的比较是将Python与Haskell进行比较。 (据记录:我爱Python,并且更喜欢C ++,但我更喜欢Haskell。)