动态类型允许哪些功能?[关闭]


91

我已经使用python几天了,我想我了解动态和静态类型之间的区别。我不了解的是在什么情况下会首选。它是灵活且易读的,但是要付出更多的运行时检查和其他必需的单元测试的代价。

除了灵活性和可读性之类的非功能性标准之外,还有什么理由选择动态类型?使用动态类型我该怎么办呢?您能想到什么特定的代码示例来说明动态类型的具体优势?


5
从理论上讲,只要语言是Turing Complete,您就什么都做不到。对我而言,更有趣的问题是,一个与另一个相对容易或自然。尽管我知道它有能力,但是我在Python中经常做的事情甚至在C ++中甚至都不会考虑。
Mark Ransom

28
正如克里斯·史密斯(Chris Smith)在其出色的论文中所写,在辩论类型系统之前要知道的是:“在这种情况下,问题在于大多数程序员的经验有限,并且没有尝试过多种语言。就上下文而言,这里有六到七种不会算作“很多”。这样做的两个有趣的结果是:(1)许多程序员使用了非常差的静态类型的语言。(2)许多程序员使用了非常差的动态类型的语言。
Daniel Pryden

3
@suslik:如果语言原语具有荒谬的类型,那么您当然可以对类型进行荒谬的操作。这与静态和动态类型之间的差异无关。
乔恩·普迪

10
@CzarekTomczak:是的,这是某些动态类型语言的功能。但是静态类型的语言可以在运行时进行修改。例如,Visual Studio允许您在调试器中处于断点时重写C#代码,甚至倒带指令指针以使用新更改重新运行代码。正如我在另一条评论中引用克里斯·史密斯(Chris Smith)的话:“许多程序员使用了非常差的静态类型语言” —不要用您所知道的语言来判断所有静态类型语言。
丹尼尔·普赖登

11
@WarrenP:您断言“动态类型系统减少了我必须输入的多余内容”,但随后将Python与C ++进行了比较。那不是一个公平的比较:当然,C ++比Python更冗长,但这不是因为它们的类型系统不同,而是因为它们的语法不同。如果您只想减少程序源中的字符数,请学习J或APL:我保证它们会更短。比较公平的比较是将Python与Haskell进行比较。(据记录:我爱Python,并且更喜欢C ++,但我更喜欢Haskell。)
Daniel Pryden 2012年

Answers:


50

既然您要举一个具体的例子,我就举一个例子。

Rob Conery的Massive ORM是400行代码。之所以这么小,是因为Rob能够映射SQL表并提供对象结果,而无需大量静态类型来镜像SQL表。这是通过使用dynamicC#中的数据类型来完成的。Rob的网页详细描述了此过程,但是很明显,在这种特定的用例中,动态键入在很大程度上负责代码的简洁性。

与Sam Saffron的Dapper相比,后者使用静态类型。的SQLMapper类单独是3000行的代码。

请注意,通常的免责声明适用,您的里程可能会有所不同;Dapper与Massive有不同的目标。我仅以此为例,说明您可以在400行代码中完成的工作,而如果没有动态类型,则可能无法实现。


动态类型使您可以将类型决定推迟到运行时。 就这样。

无论您使用动态类型的语言还是静态类型的语言,您的类型选择都必须仍然明智。除非字符串中包含数字数据,否则您不会将两个字符串加在一起并期望得到数字答案;如果字符串中不包含数字数据,那么您将得到意想不到的结果。静态类型的语言一开始就不允许您这样做。

支持静态类型语言的人指出,编译器可以在执行单行之前在编译时对代码进行大量的“合理性检查”。这是一件好事。

C#具有dynamic关键字,它使您可以将类型决策推迟到运行时,而不会在其余代码中失去静态类型安全的好处。类型推断(var)消除了始终显式声明类型的需要,从而消除了用静态类型语言编写代码的许多麻烦。


动态语言似乎更喜欢一种更具交互性的即时编程方法。没有人期望您必须编写一个类并经历一个编译周期才能键入一些Lisp代码并看着它执行。但这正是我期望在C#中所做的。


22
如果我将两个数字字符串加在一起,我仍然不会期望得到一个数字结果。
pdr 2012年

22
@Robert我同意你的大部分回答。但是,请注意,存在具有交互式read-eval-print循环的静态类型的语言,例如Scala和Haskell。C#可能不是一种特别的交互式语言。
Andres F.

14
要学会给我一个Haskell。
罗伯特·哈维

7
@RobertHarvey:如果您还没有尝试过,可能会对F#感到惊讶/印象深刻。您将获得通常使用.NET语言获得的所有(编译时)类型安全性,只是很少需要声明任何类型。F#中的类型推断超出了C#中的可用/有效范围。另外:类似于Andres和Daniel指出的那样,F#交互是Visual Studio的一部分...
Steven Evers 2012年

8
抱歉,除非字符串包含数字数据,否则您将不会将两个字符串加在一起,并且期望得到一个数字答案,否则,您将获得意想不到的结果。”抱歉,这与动态和静态类型无关,这是类型还是类型。
vartec

26

诸如“静态类型”和“动态类型”之类的短语到处都是,人们倾向于使用微妙的不同定义,所以让我们从澄清我们的意思开始。

考虑一种具有静态类型的语言,该类型在编译时进行检查。但是要说类型错误仅生成非致命的警告,并且在运行时,所有内容都是鸭子类型的。这些静态类型仅是为了程序员的方便,并不影响代码生成。这说明静态类型本身并不施加任何限制,并且与动态类型不互斥。(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(其实现需要在运行时中进行一些简化,请参见此处的讨论)。

当代码生成不依赖于静态类型,并且您拥有消息转发之类的功能时,您可以使用代理对象,调试等做很多有趣的事情。

因此,这是一些示例的样本,如果您不需要满足类型检查器的要求。这些限制不是由静态类型强加的,而是由强制执行的静态类型检查强加的。


2
“ Python 2静态类型检查器将拒绝Python 3代码(反之亦然),即使它永远不会执行。我的类型安全程序包含静态类型错误。” 听起来您真正需要的是某种“静态if”,如果条件为false,则编译器/解释器甚至看不到代码。
大卫·斯通

@davidstone存在于C ++
Milind - [R

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类型预处理器语句来执行此操作,同时在两种情况下都保持类型安全。
梅森惠勒2014年

1
@ MasonWheeler,davidstone不,预处理器技巧和static_if都太静态了。在我的示例中,我使用了Python2和Python3,但就像AmazingModule2.0和AmazingModule3.0一样,它们之间的接口在版本之间发生了变化,这很容易。您最早可以知道接口是在模块导入时,这一定是在运行时(至少在您希望支持动态链接时)。
ridiculous_fish 2014年

18

鸭子类型的变量是每个人都想到的第一件事,但是在大多数情况下,您可以通过静态类型推断获得相同的好处。

但是,以任何其他方式很难实现在动态创建的集合中键入鸭子:

>>> d = JSON.parse(foo)
>>> d['bar'][3]
12
>>> d['baz']['qux']
'quux'

那么,JSON.parse返回什么类型呢?整数或字典字符串数组的字典?不,即使这样还不够普遍。

JSON.parse必须返回某种“变量值”,该变量可以递归地为null,bool,float,string,这些类型中的任何一种的数组,或者从string递归返回这些类型中的任何一种的字典。动态类型化的主要优点来自于具有此类变体类型。

到目前为止,这是动态类型而不是动态类型语言的好处。体面的静态语言可以完美地模拟任何此类类型。(甚至“不好的”语言也常常可以通过在后台破坏类型安全性和/或要求笨拙的访问语法来模拟它们。)

动态类型语言的优点是静态类型推断系统无法推断此类类型。您必须显式地编写类型。但是,在许多此类情况下(包括一次),描述类型的代码与在不描述类型的情况下解析/构造对象的代码一样复杂,因此仍然不一定具有优势。


21
您的JSON解析示例可以轻松地由代数数据类型静态处理

2
好吧,我的答案还不够清楚。谢谢。JSValue是动态类型的显式定义,正是我在说的。有用的是那些动态类型,而不是需要动态类型的语言。但是,仍然重要的是,动态类型不能由任何实型推断系统自动生成,而大多数常见的例子都是微不足道的。我希望新版本能更好地解释它。
abarnert 2012年

4
@MattFenwick代数数据类型在实践中几乎仅限于功能语言。Java和c#之类的语言呢?
spirc 2012年

4
ADT在C / C ++中以标记的联合形式存在。这并非功能语言所独有。
克拉克·盖贝尔

2
@spirc您可以使用多个类来模拟经典的OO语言中的ADT,这些类都从一个公共接口派生,对getClass()或GetType()的运行时调用以及相等性检查。或者您可以使用双重调度,但是我认为这在C ++中会带来更多收益。因此,您可能具有JSObject接口以及JSString,JSNumber,JSHash和JSArray类。然后,您将需要一些代码将这种“未类型化”的数据结构转换为“应用程序类型”的数据结构。但是您可能也想使用动态类型的语言来执行此操作。
丹尼尔·扬科斯基

12

由于与它所关注的编程语言相比,每个远程实际使用的静态类型系统都受到严格限制,因此它无法表示可以在运行时检查的所有不变量。为了不避开类型系统试图给出的保证,因此选择保守并禁止使用情况,这些情况将通过这些检查,但不能(在类型系统中)被证明是通过的。

我举一个例子。假设您实现了一个简单的数据模型来描述数据对象,它们的集合等,该模型是静态类型的,也就是说,如果模型说x类型为Foo的对象的属性包含一个整数,则它必须始终包含一个整数。因为这是一个运行时构造,所以不能静态键入它。假设您存储YAML文件中描述的数据。您创建一个哈希映射(稍后将传递给YAML库),获取x属性,将其存储在映射中,获取恰好是字符串的其他属性,...等一下?the_map[some_key]现在是什么类型?好吧,我们知道some_key'x',因此结果必须是整数,但是类型系统甚至无法开始对此进行推理。

一些经过积极研究的类型系统可能适用于此特定示例,但是它们非常复杂(包括编译器作者的实现和程序员的推理能力),尤其是对于这种“简单”的东西(我是说,我只是在一个例子中进行了解释)段)。

当然,今天的解决方案是将所有内容装箱,然后进行强制转换(或使用一堆被覆盖的方法,其中大多数方法会引发“未实现”异常)。但这不是静态类型,它是围绕类型系统的一种技巧,可以在运行时进行类型检查。


泛型类型没有装箱要求。
罗伯特·哈维

@RobertHarvey是的。我不是在谈论Java C#中拳击,而是在谈论“将其包装在某些包装器类中,其唯一目的是代表U的子类型中的T值”。参数多态性(您称为通用类型)不适用于我的示例。这是对具体类型的编译时抽象,但是我们需要一种运行时键入机制。

值得指出的是,Scala的类型系统是图灵完整的。因此,类型系统可能比您想象的要简单。
安德里亚(Andrea)

@Andrea我故意没有将我的描述简化为完整。曾经在巡回演出中编程过吗?还是试图将这些东西编码为类型?在某些时候,它变得太复杂以至于不可行。

@delnan我同意。我只是指出类型系统可以完成相当复杂的工作。我的印象是,您的回答意味着类型系统只能进行简单的验证,但是在第二遍阅读中,您并未编写任何类似内容!
安德里亚(Andrea)2012年

7

使用动态类型不能执行与使用静态类型不能执行的任何操作,因为可以在静态类型的语言之上实现动态键入。

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
杰德2012年

5
您将示例中的动态类型化与弱类型化混合在一起。动态类型是关于对未知类型进行操作,而不是定义允许的类型列表以及这些类型之间的重载操作。
小牛

2
@Jed一旦实现了对象模型,基本类型和原始操作,就不需要其他基础工作了。您可以轻松,自动地将原始动态语言的程序翻译成该方言。
NovaDenizen 2012年

2
@hcalves由于您是在Haskell代码中提到重载,因此我怀疑您对它的语义不太了解。我定义了一个新的+运算符,该运算符将两个Data值合并为另一个Data值。 Data代表动态类型系统中的标准值。
NovaDenizen 2012年

1
@Jed:大多数动态语言都有少量的“原始”类型和一些引入新值(数据结构,如列表)的归纳方式。例如,Scheme远远超出了原子,对和向量。您应该能够以与给定动态类型的其余部分相同的方式来实现这些功能。
Tikhon Jelvis '10 -10-10

3

膜是围绕整个对象图的包装,而不是仅用于单个对象的包装。通常,膜的创建者开始只是将单个对象包裹在膜中。关键思想是,穿过膜的任何对象参照物本身都将被可传递地包裹在同一膜中。

在此处输入图片说明

每种类型都由具有相同接口的类型包装,但是它会截取消息并在跨膜时包装和拆开值。您最喜欢的静态类型化语言中的wrap函数的类型是什么?也许Haskell具有该功能的类型,但是大多数静态类型的语言却没有,或者最终使用Object→Object,从而有效地放弃了其作为类型检查器的责任。


4
是的,Haskell确实可以使用存在性类型来实现这一点。如果您有一些类型类Foo,则可以对实例化该接口的任何类型进行包装。 class Foo a where ... data Wrapper = forall a. Foo a => Wrapper a
杰克·麦克阿瑟

@JakeMcArthur,感谢您的解释。那是我坐下来学习Haskell的另一个原因。
Mike Samuel

2
您的膜是一个“接口”,并且对象的类型是“现有类型”的-也就是说,我们知道它们存在于该接口下,但是仅此而已。自80年代以来就已经知道数据抽象的现有类型。参考文献cs.cmu.edu/~rwh/plbook/book.pdf第21.1章
Don Stewart

@DonStewart。那么Java代理类是存在类型机制吗?膜变得困难的一个地方是使用标称类型系统的语言,这些系统在该类型的定义之外可以看到具体类型的名称。例如,一个人不能包装,String因为它是Java中的一种具体类型。Smalltalk不会出现此问题,因为它不会尝试键入#doesNotUnderstand
Mike Samuel

1

就像有人提到的那样,如果您自己实现某些机制,那么从理论上讲,与动态类型相比,您无能为力。大多数语言都提供类型松弛机制来支持类型灵活性,例如空指针,根对象类型或空接口。

更好的问题是,为什么在某些情况和问题下动态类型更合适,更合适。

首先,让我们定义

实体 -我需要代码中一些实体的一般概念。它可以是任何类型,从原始数到复杂数据。

行为 -可以说我们的实体具有某种状态和一组方法,这些方法和方法可以让外界指示实体做出某些反应。让我们将此实体的状态+接口称为行为。一个实体可以具有工具语言提供的以某种方式组合的多个行为

实体及其行为的定义 -每种语言都提供了某种抽象方法,可帮助您定义程序中某些实体的行为(方法集+内部状态)。您可以为这些行为指定一个名称,并说所有具有此行为的实例都是特定类型的

这可能不是那么陌生。正如您所说,您理解了差异,但仍然如此。可能还不完整和最准确的解释,但我希望足够有趣以带来一些价值:)

静态类型化 -在开始运行代码之前,在编译时检查程序中所有实体的行为。这意味着,例如,如果您想要类型为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)灵活得多的建模工具。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.