是否只有在动态类型化语言(如Python)中才有可能的设计模式?


30

我读过一个相关的问题,在动态语言(如Python)中是否有不需要的设计模式?并记得Wikiquote.org上的这句话

动态类型的妙处在于它使您可以表达任何可计算的内容。而类型系统则不是-类型系统通常是可决定的,它们将您限制为一个子集。支持静态类型系统的人说:“很好,足够好;您要编写的所有有趣程序都将作为类型使用。” 但这很荒谬—一旦有了类型系统,您甚至都不知道那里有什么有趣的程序。

---软件工程电台第140集:Gilad Bracha的Newspeak和可插拔类型

我想知道,是否有有用的设计模式或策略使用引号的表述“不能作为类型工作”?


3
我发现在静态类型的语言中很难完成双重调度和Visitor模式,但在动态语言中则很容易实现。参见以下答案(和问题),例如:programmers.stackexchange.com/a/288153/122079
user3002473 '16

7
当然。例如,任何涉及在运行时创建新类的模式。(这在Java中也是可能的,但在C ++中是不可能的;动态范围在不断变化)。
user253751 '16

1
这将在很大程度上取决于类型系统的复杂程度:-)函数语言通常会在此方面做得很好。
Bergi

1
每个人似乎都在谈论Java和C#之类的类型系统,而不是Haskell或OCaml。具有强大类型系统的语言可以像动态语言一样简洁,但是可以保持类型安全。
安德鲁说莫妮卡(Monica)

@immibis那是不对的。静态类型系统可以在运行时绝对创建新的“动态”类。请参阅《编程语言实用基础》的第33章。
gardenhead's

Answers:


4

一流的类型

动态类型意味着您拥有一流的类型:您可以在运行时检查,创建和存储类型,包括语言本身的类型。这也意味着是类型化的,不是变量

静态类型的语言可能还会生成依赖于动态类型的代码,例如方法分派,类型类等,但其运行时通常不可见。充其量,它们为您提供了进行内省的方式。另外,您可以将类型模拟为值,但随后会有一个临时动态类型系统。

但是,动态类型系统很少只有一流的类型。您可以拥有一流的符号,一流的软件包,一流的……。一切。这与静态类型语言中的编译器语言和运行时语言之间的严格分隔形成对比。编译器或解释器可以在运行时执行的操作也可以执行。

现在,让我们同意类型推断是一件好事,并且我希望在运行它之前检查我的代码。但是,我也喜欢能够在运行时生成和编译代码。而且我也喜欢在编译时预先计算事物。在动态类型的语言中,这是使用相同的语言完成的。在OCaml中,您具有模块/函数类型系统,该系统与主类型系统不同,而主系统与预处理器语言不同。在C ++中,您有与主要语言无关的模板语言,而主要语言通常在执行过程中不了解类型。而这罚款在这些语言,因为他们不想提供更多。

最终,这并没有改变真的有什么,你可以开发一种软件,但表现力的变化如何,你开发他们无论是硬或没有。

模式

依赖于动态类型的模式是涉及动态环境的模式:开放类,调度,对象的内存数据库,序列化等。诸如通用容器之类的简单事情之所以起作用,是因为向量在运行时不会忘记它所持有的对象的类型。 (无需参数类型)。

我试图介绍在Common Lisp中评估代码的多种方式,以及可能进行静态分析的示例(这是SBCL)。沙箱示例将编译从单独文件中提取的一小部分Lisp代码。为了安全起见,我更改了readtable,只允许使用标准符号的一部分,并用超时包装了东西。

;;
;; Fetching systems, installing them, etc. 
;; ASDF and QL provide provide resp. a Make-like facility 
;; and system management inside the runtime: those are
;; not distinct programs.
;; Reflexivity allows to develop dedicated tools: for example,
;; being able to find the transitive reduction of dependencies
;; to parallelize builds. 
;; https://gitlab.common-lisp.net/xcvb/asdf-dependency-grovel
;;
(ql:quickload 'trivial-timeout)

;;
;; Readtables are part of the runtime.
;; See also NAMED-READTABLES.
;;
(defparameter *safe-readtable* (copy-readtable *readtable*))
(set-macro-character #\# nil t *safe-readtable*)
(set-macro-character #\: (lambda (&rest args)
                           (declare (ignore args))
                           (error "Colon character disabled."))
                     nil
                     *safe-readtable*)

;; eval-when is necessary when compiling the whole file.
;; This makes the result of the form available in the compile-time
;; environment. 
(eval-when (:compile-toplevel :load-toplevel :execute)
  (defvar +WHITELISTED-LISP-SYMBOLS+ 
    '(+ - * / lambda labels mod rem expt round 
      truncate floor ceiling values multiple-value-bind)))

;;
;; Read-time evaluation #.+WHITELISTED-LISP-SYMBOLS+
;; The same language is used to control the reader.
;;
(defpackage :sandbox
  (:import-from
   :common-lisp . #.+WHITELISTED-LISP-SYMBOLS+)
  (:export . #.+WHITELISTED-LISP-SYMBOLS+))

(declaim (inline read-sandbox))

(defun read-sandbox (stream &key (timeout 3))
  (declare (type (integer 0 10) timeout))
  (trivial-timeout:with-timeout (timeout)
    (let ((*read-eval* nil)
          (*readtable* *safe-readtable*)
          ;;
          ;; Packages are first-class: no possible name collision.
          ;;
          (package (make-package (gensym "SANDBOX") :use '(:sandbox))))
      (unwind-protect
           (let ((*package* package))
             (loop
                with stop = (gensym)
                for read = (read stream nil stop)
                until (eq read stop)
                ;;
                ;; Eval at runtime
                ;;
                for value = (eval read)
                ;;
                ;; Type checking
                ;;
                unless (functionp value)
                do (error "Not a function")
                ;; 
                ;; Compile at run-time
                ;;
                collect (compile nil value)))
        (delete-package package)))))

;;
;; Static type checking.
;; warning: Constant 50 conflicts with its asserted type (MOD 11)
;;
(defun read-sandbox-file (file)
  (with-open-file (in file)
    (read-sandbox in :timeout 50)))

;; get it right, this time
(defun read-sandbox-file (file)
  (with-open-file (in file)
    (read-sandbox in)))

#| /tmp/plugin.lisp
(lambda (x) (+ (* 3 x) 100))
(lambda (a b c) (* a b))
|#

(read-sandbox-file #P"/tmp/plugin.lisp")

;; 
;; caught COMMON-LISP:STYLE-WARNING:
;;   The variable C is defined but never used.
;;

(#<FUNCTION (LAMBDA (#:X)) {10068B008B}>
 #<FUNCTION (LAMBDA (#:A #:B #:C)) {10068D484B}>)

以上与其他语言无关。Blender中的插件方法,在音乐软件或IDE中用于动态编译的静态编译语言等,动态语言代替了外部工具,而是使用了利用已有信息的工具。所有已知的FOO呼叫者?BAR的所有子类?ZOT类专用的所有方法?这是内部数据。类型只是此方面的另一个方面。


(另请参见:CFFI


39

简短的回答:不,因为图灵等效。

长答案:这个家伙真是个巨魔。虽然类型系统确实“将您限制为一个子集”,但根据定义,该子集之外的内容是无效的。

您可以使用任何图灵完备的编程语言(这是为通用编程设计的语言,再加上其他很多都不具备的功能)可以做的一切;要清除的门槛非常低,并且有几个示例说明系统正在成为图灵-您可以使用其他任何图灵完备的编程语言来完成此操作。这称为“图灵等效”,仅表示其确切含义。重要的是,这并不意味着您可以用另一种语言轻松完成另一件事-有人认为,这首先就是创建新编程语言的全部意义:为您提供一种更好的方式来完成某些任务现有语言无法吸收的东西。

例如,可以通过将所有变量,参数和返回值声明为基本Object类型,然后使用反射来访问其中的特定数据,从而在静态OO类型系统的顶部上模拟动态类型系统。您会发现,使用动态语言几乎无法执行任何操作,而使用静态语言则无法执行任何操作。但是,这样做当然会很麻烦。

引号中的家伙是正确的,静态类型限制了您可以执行的操作,但这是一个重要功能,而不是问题。道路上的线路限制了您在汽车上的工作能力,但是您发现它们对我们有限制还是有用?(我知道我不想在繁忙,复杂的道路上开车,那里没有任何东西告诉汽车沿相反的方向行驶并保持在一边,而不会越过我的驾驶位置!)通过制定规则来清楚地描述出什么被认为是无效行为,并确保不会发生这种情况,您可以大大减少发生令人讨厌的崩溃的机会。

另外,他在误导对方。这并不是说“您要编写的所有有趣程序都将作为类型来工作”,而是“您要编写的所有有趣程序都将需要类型”。一旦达到某种程度的复杂性,就很难在没有类型系统来保持一致的情况下维护代码库,这有两个原因。

首先,因为没有类型注释的代码很难阅读。考虑以下Python:

def sendData(self, value):
   self.connection.send(serialize(value.someProperty))

您希望连接另一端的系统收到的数据看起来像什么?而且,如果收到的东西看起来完全不对,您如何确定发生了什么?

一切都取决于的结构value.someProperty。但是它看起来像什么?好问题!叫sendData()什么 它过去了什么?该变量是什么样的?它从哪里来的?如果它不是本地的,则必须追溯整个历史,value以跟踪正在发生的事情。也许您正在传递也具有someProperty属性的其他东西,但是它没有按照您的想象做?

现在,让我们用类型注释来查看它,就像您在Boo语言中可能会看到的那样,它使用非常相似的语法,但是是静态类型的:

def SendData(value as MyDataType):
   self.Connection.Send(Serialize(value.SomeProperty))

如果出了什么问题,突然之间您的调试工作就容易了一个数量级:查找MyDataType!的定义。另外,由于传递了一些不兼容的类型而该属性也具有相同名称的属性而导致不良行为的可能性突然变为零,这因为类型系统不会让您犯该错误。

第二个原因建立在第一个原因上:在一个大型而复杂的项目中,您很可能拥有多个贡献者。(如果不是,您将花费很长的时间自己构建它,这基本上是同一回事。如果您不相信我,请尝试阅读3年前编写的代码!)这意味着您不知道那是什么遍历编写代码时几乎编写任何给定部分的人的头,因为您不在那儿,或者不记得它是很久以前的代码了。具有类型声明确实可以帮助您了解代码的意图!

在报价中,喜欢引用的人经常将静态类型的好处误解为“帮助编译器”或“所有关于效率”,在这个世界中,几乎无限的硬件资源与过去的每一年越来越不相关。但是,正如我所展示的,尽管这些好处确实存在,但主要的好处在于人为因素,尤其是代码的可读性和可维护性。(不过,提高效率肯定是一个不错的奖励!)


24
“这家伙真是个巨魔。” –我不确定人道攻击是否会帮助您以其他方式表现良好的案件。而且,尽管我很清楚,权威人士的论点与ad hominem一样是同样糟糕的谬论,但我仍然要指出,Gilad Bracha可能比大多数人设计了更多的语言,并且(对于本次讨论最相关)更多的静态类型系统。只是一个小的摘录:他是新话,飞镖,Java语言规范和Java虚拟机规范的合着者,Java和JVM中,设计工作的设计师合作的唯一设计师设计的...
约尔格W¯¯米塔格

10
Strongtalk(用于Smalltalk的静态类型系统),Dart类型系统,Newspeak类型系统,他关于模块化的博士学位论文几乎是每个现代模块系统(例如Java 9,ECMAScript 2015,Scala,Dart,Newspeak,Ioke)的基础(Seph),他的关于mixin的论文彻底改变了我们对它们的思考方式。现在,这并不能意味着他是对的,但我认为有设计多个静态类型系统让他多了一个“巨魔”多一点。
约尔格W¯¯米塔格

17
“虽然类型系统确实“将您限制为一个子集”,但是根据定义,该子集之外的东西是不起作用的东西。” –这是错误的。从暂停问题的不确定性,赖斯定理以及无数其他不确定性和不可计算性结果中我们知道,静态类型检查器无法确定所有程序的类型安全还是类型安全。它不能接受那些程序(其中一些是类型不安全的),因此唯一明智的选择是拒绝它们(但是,其中一些是类型安全的)。或者,必须在以下语言中设计语言
–JörgW Mittag

9
……这种方式使程序员无法编写那些不确定的程序,但同样,其中一些实际上是类型安全的。因此,无论如何分割它:程序员都无法编写类型安全的程序。而且,由于实际上有无穷多的人(通常情况下),我们几乎可以肯定的是,至少其中一些不仅东西,的工作,但也很有用。
约尔格W¯¯米塔格

8
@MasonWheeler:总是在静态类型检查的情况下出现暂停问题。除非精心设计语言以防止程序员编写某些类型的程序,否则静态类型检查将很快等同于解决暂停问题。无论你最终你不能写,因为它们可能会混淆类型检查,或者你结束了你的程序的程序可以写,但他们采取的是无限量的类型检查。
约尔格W¯¯米塔格

27

我将回避“模式”部分,因为我认为它涉及到什么是模式或什么不是模式的定义,而我早就对该辩论失去了兴趣。我要说的是,有些语言可以完成某些事情,而其他语言则无法做到。让我清楚一点,我并不是说有些问题可以用一种语言解决,而其他语言则不能解决。梅森已经指出了图灵的完整性。

例如,我用python编写了一个类,该类将XML DOM元素包装起来并使其成为第一类对象。也就是说,您可以编写代码:

doc.header.status.text()

并且您从解析的XML对象中获得了该路径的内容。海事组织有点整洁。而且,如果没有头节点,它只会返回一个虚拟对象,该虚拟对象只包含虚拟对象(一直到处都是乌龟)。在Java中,没有真正的方法可以做到这一点。您必须根据对XML结构的一些知识提前编译一个类。抛开这是否一个好主意,这种事情确实确实改变了您使用动态语言解决问题的方式。我并不是说它以一定总是更好的方式改变。动态方法有一定的成本,梅森的答案给出了不错的概述。它们是否是一个好的选择取决于许多因素。

附带说明一下,您可以在Java中执行此操作,因为您可以在Java中构建python解释器。当人们谈论图灵完备性时,以给定语言解决特定问题可能意味着要构建口译员或与其类似的事情,这一事​​实常常被忽略。


4
您无法在Java中执行此操作,因为Java设计不良。使用C#在C#中并不是那么困难IDynamicMetaObjectProvider,而在Boo中却变得非常简单。(这是少于100行的实现,包含在GitHub上的标准源代码树中,因为这很容易!)
Mason Wheeler

6
@MasonWheeler "IDynamicMetaObjectProvider"?与C#的dynamic关键字有关吗?...这实际上只是将动态类型添加到C#?如果我是对的,不确定您的论点是否有效。
jpmc26

9
@MasonWheeler您正在了解语义。在不讨论细节问题的情况下(这里我们不开发关于SE的数学形式主义。),动态类型化是前面围绕类型进行编译时决策的一种做法,尤其是验证每种类型都具有程序访问的特定成员。那就是dynamic用C#实现的目标。“反射和字典查找”发生在运行时,而不是编译时。我真的不知道如何确定它不会为该语言添加动态类型。我的观点是,吉米的最后一段涵盖了这一点。
jpmc26

44
尽管不是Java的忠实拥护者,但我还敢说将Java称为“设计不良” 因为它没有添加动态类型,这实在是太过热情了。
jpmc26

5
除了稍微方便一些的语法外,这与字典有何不同?
Theodoros Chatzigiannakis

10

报价是正确的,但确实很虚假。让我们分解一下原因:

动态类型的妙处在于它使您可以表达任何可计算的内容。

好吧,不完全是。具有动态类型的语言可以让您表达任何东西,只要它是Turing complete即可。类型系统本身不允许您表达所有内容。让我们在这里给他带来疑问的好处。

而类型系统则不是-类型系统通常是可决定的,它们将您限制为一个子集。

的确如此,但是请注意,我们现在正在坚定地讨论什么类型系统允许的内容,而不是使用类型系统的语言所允许的内容。尽管可以在编译时使用类型系统来计算填充,但是通常这不是图灵完成的(因为类型系统通常是可确定的),但是几乎所有静态类型化语言在其运行时也都是图灵完成的(依赖类型的语言是不是,但是我不相信我们在这里谈论它们)。

支持静态类型系统的人说:“很好,足够好;您要编写的所有有趣程序都将作为类型使用。” 但这很荒谬—一旦有了类型系统,您甚至都不知道那里有什么有趣的程序。

问题在于动态类型语言确实具有静态类型。有时,所有内容都是字符串,更常见的是带有一些标记的并集,其中所有内容都是一袋属性或诸如int或double之类的值。麻烦的是静态语言也可以做到这一点,从历史上看,这样做有点笨拙,但是现代的静态类型的语言使此操作与使用动态类型的语言一样容易,因此如何在其中有所区别程序员可以看到什么有趣的程序?静态语言具有完全相同的标记联合以及其他类型。

要回答标题中的问题:不,没有不能用静态类型的语言实现的设计模式,因为您总是可以实现足够多的动态系统来获得它们。您可能会以一种动态语言获得一些“免费”的模式;对于YMMV来说,这可能或不值得忍受这些语言的缺点。


2
我不确定您是否回答是或否。对我来说听起来更像是不。
user7610

1
@TheodorosChatzigiannakis是的,动态语言还将如何实现?首先,如果您想实现动态类系统或其他任何涉及的内容,则需要一名宇航员建筑师。其次,您可能没有足够的资源来使其可调试,完全自省,高性能(“仅使用字典”就是实现慢速语言的方式)。第三,在集成到整个语言中时最好使用某些动态功能,而不仅仅是将其作为库:例如,垃圾回收为例(有GC作为库,但并不常用)。
coredump

1
@Theodoros根据我曾经在这里链接过的论文,除了2.5%的结构(在研究的Python模块中)之外,所有结构都可以轻松地用一种类型化的语言表示。也许2.5%值得付出动态打字的费用。这本质上就是我的问题。neverworkintheory.org/2016/06/13/polymorphism-in-python.html
user7610

3
@JiriDanek据我所知,没有什么可以阻止静态类型的语言具有多态调用点并在此过程中保持静态类型。请参阅多方法的静态类型检查。也许我误会了您的链接。
Theodoros Chatzigiannakis

1
“与动态类型语言可以让你表达什么,只要它是图灵完整,其中大部分都是。”虽然这当然是一个真正的声明,它并没有真正持有“现实世界”,因为数额文字之一有写可能会非常大。
Daniel Jour

4

当然,有些事情只能用动态类型的语言来完成。但是它们不一定是好的设计。

您可以先为同一变量分配一个整数5,然后分配一个字符串'five'或一个Cat对象。但是,这只会使代码阅读者更难弄清到底发生了什么,每个变量的目的是什么。

您可以向库Ruby类添加新方法并访问其私有字段。在某些情况下,这样的黑客攻击可能会有用,但这将违反封装。(我不介意添加仅依赖于公共接口的方法,但这并不是静态类型的C#扩展方法无法做到的。)

您可以将新字段添加到其他人的类的对象中,以随其传递一些额外的数据。但是最好只创建一个新结构或扩展原始类型。

通常,您希望代码保留得越有条理,则能够动态更改类型定义或将不同类型的值分配给同一变量应获得的好处就越少。但是,您的代码与使用静态类型的语言所获得的代码没有什么不同。

动态语言最擅长的是语法糖。例如,在读取反序列化的JSON对象时,您可以简单地引用嵌套值,obj.data.article[0].content而不是说obj.getJSONObject("data").getJSONArray("article").getJSONObject(0).getString("content")

Ruby开发人员尤其可以详细讨论通过实现可以实现的魔术method_missing,这是一种允许您处理未声明方法的调用的方法。例如,ActiveRecord ORM使用它,因此您User.find_by_email('joe@example.com')无需声明find_by_email方法就可以进行呼叫。当然,没有什么能像UserRepository.FindBy("email", "joe@example.com")静态类型语言那样实现,但是您不能否认它的简洁。


4
当然,有些事情只能用静态类型的语言来完成。但是它们不一定是好的设计。
coredump

2
关于语法糖的要点与动态类型以及语法无关的任何事情都没有关系。
左右约

@leftaroundabout模式与语法有关。类型系统也有很多关系。
user253751 '16

4

动态代理模式是实现代理对象的快捷方式,不需要为每种类型的代理都需要一个类。

class Proxy(object):
    def __init__(self, obj):
        self.__target = obj

    def __getattr__(self, attr):
        return getattr(self.__target, attr)

使用此方法,Proxy(someObject)创建一个行为与相同的新对象someObject。显然,您还希望以某种方式添加其他功能,但这是一个有用的基础。在完整的静态语言中,您需要为要代理的每种类型编写一个Proxy类,或者使用动态代码生成(诚然,该代码已包含在许多静态语言的标准库中,主要是因为他们的设计师知道无法解决此问题的原因)。

动态语言的另一个用例是所谓的“猴子补丁”。在许多方面,这是一种反模式,而不是一种模式,但是如果仔细进行操作,它可以有用的方式使用。尽管没有理论上的理由无法以静态语言实现猴子补丁,但我从未见过真正拥有它的人。


我想我也许可以在Go中模仿这一点。所有代理对象必须具有一组方法(否则,鸭子可能不会发出嘎嘎声,而所有对象都将崩溃)。我可以使用这些方法创建Go接口。我将不得不对此进行更多的思考,但是我认为我的想法会起作用。
user7610

您可以使用RealProxy和泛型在任何.NET语言中进行类似操作。
LittleEwok

@LittleEwok-RealProxy使用运行时代码生成-就像我说的那样,许多现代静态语言都有这样的解决方法,但是在动态语言中仍然更容易。
Jules

C#扩展方法有点像使猴子补丁变得安全。您不能更改现有方法,但是可以添加新方法。
安德鲁说莫妮卡(Monica)

3

是的,有许多模式和技术只能在动态类型化的语言中实现。

猴子修补是一种在运行时将属性或方法添加到对象或类的技术。这种技术在静态类型语言中是不可能的,因为这意味着无法在编译时验证类型和操作。或者换一种说法,如果一种语言支持猴子补丁,那么它就定义为动态语言。

可以证明,如果一种语言支持猴子补丁(或类似的在运行时修改类型的技术),则不能对其进行静态类型检查。因此,这不仅是当前现有语言的局限性,还是静态类型化的基本局限性。

因此,引用肯定是正确的-动态语言比静态类型语言可能做的事情更多。另一方面,某些类型的分析只能在静态类型的语言中进行。例如,您始终知道在给定类型上允许哪些操作,这使您可以在编译类型下检测非法操作。当可以在运行时添加或删除操作时,无法用动态语言进行这种验证。

这就是为什么静态和动态语言之间没有明显的“最佳”冲突的原因。静态语言在运行时放弃了某些功能,以换取在编译时使用的另一种功能,他们相信,这些语言可以减少错误的数量并简化开发。有些人认为权衡是值得的,而其他人则不值得。

其他答案也表明,图灵等效性意味着一种语言的所有可能性在所有语言中都是可能的。但这并没有随之而来。为了支持静态语言中的猴子修补等功能,您基本上必须在静态语言中实现动态子语言。这当然是可能的,但是我认为您随后将使用嵌入式动态语言进行编程,因为您还会丢失宿主语言中存在的静态类型检查。

从版本4开始的C#支持动态类型的对象。显然,语言设计师可以从两种类型的输入中受益。但这也表明您也不能吃蛋糕:当您在C#中使用动态对象时,您将具有执行诸如猴子修补之类的功能的能力,但是您也失去了与这些对象进行交互的静态类型验证。


我认为您的倒数第二段+1是至关重要的论据。我仍然认为,是有区别的,虽然与静态类型你必须在那里,什么事都能猴补丁的完全控制
JK。

2

我想知道,是否有有用的设计模式或策略使用引号的表述“不能作为类型工作”?

是的,没有。

在某些情况下,程序员比编译器更精确地知道变量的类型。编译器可能知道某物是一个对象,但是程序员会(由于程序的不变性)知道它实际上是一个字符串。

让我展示一些例子:

Map<Class<?>, Function<?, String>> someMap;
someMap.get(object.getClass()).apply(object);

我知道someMap.get(T.class)会返回Function<T, String>,因为我是如何构造someMap的。但是Java只确定我有一个Function。

另一个例子:

data = parseJSON(someJson)
validate(data, someJsonSchema);
print(data.properties.rowCount);

我知道data.properties.rowCount将是一个有效的引用和一个整数,因为我已经根据模式验证了数据。如果缺少该字段,则将引发异常。但是编译器只会知道是抛出异常还是返回某种通用JSONValue。

另一个例子:

x, y, z = struct.unpack("II6s", data)

“ II6”定义了数据对三个变量进行编码的方式。由于已经指定了格式,因此我知道将返回哪些类型。编译器只会知道它返回一个元组。

所有这些示例的统一主题是程序员知道类型,但是Java级别的类型系统将无法反映该类型。编译器不会知道类型,因此,静态类型的语言不会让我称呼它们,而动态类型的语言会告诉我。

那就是它的原始报价:

动态类型的妙处在于它使您可以表达任何可计算的内容。而类型系统则不是-类型系统通常是可决定的,它们将您限制为一个子集。

使用动态类型时,我可以使用我所知道的最派生类型,而不仅仅是我语言的类型系统所知道的最派生类型。在上述所有情况下,我的代码在语义上都是正确的,但是将被静态类型系统拒绝。

但是,回到您的问题:

我想知道,是否有有用的设计模式或策略使用引号的表述“不能作为类型工作”?

通过添加适当的强制类型转换,可以使上述示例中的任何示例以及实际上任何类型的动态类型在静态类型中均有效。如果您知道编译器不知道的类型,只需通过转换值来告诉编译器。因此,在某种程度上,您将不会使用动态类型来获得任何其他模式。您可能只需要投放更多内容即可使用静态类型的代码。

动态类型化的优点是,您可以简单地使用这些模式而不必担心事实,因为它很难使类型系统确信其有效性。它不会改变可用的模式,只是可能使它们更易于实现,因为您不必弄清楚如何使类型系统识别模式或添加强制转换来颠覆类型系统。


1
为什么java是您不应该使用“更高级/更复杂的类型系统”的起点?
jk。

2
@jk,是什么导致您认为这就是我的意思?我明确地避免考虑是否应该使用更高级/更复杂的类型系统。
Winston Ewert

2
其中一些是可怕的例子,其他似乎更多的是语言决定,而不是打字还是不打字。我特别困惑于人们为什么认为类型化语言中的反序列化是如此复杂。data = parseJSON<SomeSchema>(someJson); print(data.properties.rowCount); 如果没有一个要反序列化的类,则输入的结果将是data = parseJSON(someJson); print(data["properties.rowCount"]);- 我们可以退回到-仍然输入并表达相同意图的类。
NPSF3000'8

2
@ NPSF3000,parseJSON函数如何工作?它似乎使用反射或宏。如何以静态语言键入data [“ properties.rowCount”]?它怎么知道结果值是整数?
Winston Ewert

2
@ NPSF3000,如果不知道它是整数,您打算如何使用它?在不知道它是数组的情况下,如何计划在JSON列表中的元素上循环?我的例子的重点是我知道那data.properties是一个对象,我知道那data.properties.rowCount是一个整数,我可以简单地编写使用它们的代码。您的提议data["properties.rowCount"]没有提供相同的内容。
温斯顿·埃韦特

1

以下是一些来自Objective-C(动态类型)的示例,这些示例在C ++(静态类型)中是不可能的:

  • 将几个不同类的对象放入同一容器中。
    当然,这需要运行时类型检查来随后解释容器的内容,并且大多数静态类型的朋友都会反对您一开始就不应该这样做。但是我发现,除了宗教辩论之外,这可以派上用场。

  • 扩展类而不进行子类化。
    在Objective-C中,您可以为现有类定义新的成员函数,包括由语言定义的类NSString。例如,您可以添加一个method stripPrefixIfPresent:,以便您可以说[@"foo/bar/baz" stripPrefixIfPresent:@"foo/"](注意NSSring文字的使用@"")。

  • 使用面向对象的回调。
    在Java和C ++之类的静态类型语言中,必须花相当长的时间才能允许库调用用户提供的对象的任意成员。在Java中,解决方法是接口/适配器对加上一个匿名类,在C ++中,解决方法通常基于模板,这意味着库代码必须公开给用户代码。在Objective-C中,您只需将对象引用以及方法的选择器传递给库,该库就可以简单直接地调用回调。


我可以通过强制转换为void *在C ++中进行第一个操作,但这绕过了类型系统,因此不算在内。我可以使用扩展方法在C#中完成第二个任务,完全可以在类型系统中完成。第三,我认为“方法的选择器”可以是lambda,因此,如果我理解正确的话,任何带有lambda的静态类型语言都可以做到这一点。我不熟悉ObjC。
user7610

1
@JiriDanek“我可以通过强制转换为void *在C ++中做第一件事”,不完全是,读取元素的代码无法自行检索实际类型。您需要类型标签。此外,我不认为说“我可以用<language>做到这一点”是查看此问题的适当/有效方式,因为您总是可以模仿它们。重要的是表达能力的提高与实现的复杂性。另外,您似乎认为如果一种语言同时具有静态和动态功能(Java,C#),则它仅属于“静态”语言家族。
coredump

1
@JiriDanek void*本身不是动态键入,而是缺少键入。但是,是的,dynamic_cast,虚拟表等使C ++并非纯粹是静态类型的。那不好吗?
coredump

1
它建议在需要时可以选择颠覆类型系统。在需要时使用逃生口。或有人认为这很有用。否则,他们将不会将其放入语言中。
user7610

2
@JiriDanek我认为,您几乎在最后一条评论中将其定为钉子。如果小心使用,那些逃生舱口可能非常有用。但是,强大的权力伴随着巨大的责任,滥用它的人很多。因此,使用指向所有其他类都是根据定义派生的通用基类的指针感觉要好得多(情况就是如此) (在Objective-C和Java中),并依靠RTTI区分情况,而不是将a强制转换void*为某些特定的对象类型。如果您搞砸了,则前者会产生运行时错误,而后者则会导致未定义的行为。
cmaster
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.