Martin Odersky向Frank Sommers和Bill Venners谈论Scala类型系统背后的设计动机。

\\

Scala是一种新兴的通用用途、类型安全的Java平台语言,结合了面向对象和函数式编程。它是洛桑联邦理工大学教授Martin Odersky的心血结晶。本访谈系列由多部分组成,由Artima网站的Frank Sommers和Bill Venners向Martin Odersky讨教Scala。在第一部分Scala起源中(点击查看《Scala起源》中文翻译),Odersky讲述了导致Scala诞生的那些历史。在第二部分Scala的设计目标中,Odersky讨论Scala设计中的妥协、目标、创新和优势。在本期中,他将挖掘Scala的类型系统的设计动机。

\\

Scala的“可伸缩性(scalability)”价值

\\

Frank Sommers: 去年JavaOne大会上,你声称Scala是一种“可伸缩的语言”,既可以用于小规模程序,也可以用于大规模程序。像我这样的程序员,使用类似这样的语言,有什么好处?

\\

Martin Odersky: Scala带来的帮助,就是让你不必混用多种专用语言。无论小型程序还是大型程序,无论通用用途还是特定应用领域,你都可以只用这一种语言。这意味着,你不用操心如何在不同语言环境中传递数据。

\\

如果你想要跨越边界传递数据,按现在的业界惯例,你往往会陷入底层实现的重重泥潭中。比如,如果你想用JDBC,从Java向数据库发起一次SQL查询,那么你发出的查询最终会是个字符串。这就意味着你的程序中只要有小小的拼写错误,在最终运行时,就会表现为一次非法查询,很可能就导致最终客户网站出错。整个过程中编译器或类型系统并不会告诉你,你某处写错了。这非常脆弱和危险。所以,如果你只用一种语言,会有很多好处。

\\

另一个问题是工具。如果您只使用一种语言,那么你只需要面对一套环境和工具。而如果你有多种不同的语言,你就必须混合并适配多套环境,构建过程也变得更复杂、更困难。

\\

Scala的可扩展性(extensibility)

\\

Frank Sommers: 上次演讲你还提到了可扩展性。你说Scala很容易扩展。你能解释一下吗?同样再问一句,这对程序员有什么好处?

\\

Martin Odersky: 可伸缩性的维度是“从小到大”。除此之外,我觉得还有另一概念“可扩展性”,表示“从通用用途到特定需求”。你需要强化Scala语言,使之涵盖你特别关注的领域。

\\

举个例子,数字类型。不同领域有很多特殊的数字类型——比如,密码学家用的大数、商务人士用的十进制大数,科学家用的复数——这样的例子不胜枚举。上述每个群体都会真正深切关注他们所需的类型,但作为一门语言,如果加入了所有类型,就会过于笨重。

\\

怎么办呢?当然我们可以说,这样吧,把这些类型留给外部库实现吧。不过,如果你真的关心某个应用领域,那么你会希望,调用这些库的代码,看起来能像调用内置类型的代码一样干净、优雅。为此,你需要语言提供某些扩展机制,使你可以编写用起来感觉不像库的库。对库用户来说,比方说,使用某个十进制大数库中的BigDecimal类型时,应该像使用内置的Int一样方便。

\\

小规模编程中的类型

\\

Frank Sommers: 先前你提到,在使用单一语言而非混用多语言的场合,类型尤为重要。我觉得大部分人都认可,大规模编程时,类型确有其效。在超大型程序中,类型能帮你组织程序,保证改代码不会把程序搞坏。但是,小规模编程的场合下我们为什么还要用类型?比如只编写一段脚本时?对这种程度的编程,类型重要吗?

\\

Martin Odersky: 小规模程序恐怕类型真没那么重要。类型的价值分布在一条长长的光谱上,一端表示超级有用,一端表示极度麻烦。通常情况下,说它麻烦,是因为类型定义可能会太过冗余,要求你手动指定大量类型。说它有用,则是因为,类型能避免运行时错误,类型能提供API签名文档,类型能为代码重构提供安全底线。

\\

Scala的类型推断,试图尽可能减少类型的麻烦之处。这意味着,你编写脚本时并不需要涉及类型。因为即使你不指名类型,系统仍会为你推断出类型。同时,编译器内部仍然会考虑类型。所以你写的脚本如果有类型错误,编译器将发现错误,为你提供错误信息。而且,我相信,不管脚本还是大型系统,依靠编译器提示及早修复错误,总比推后错误要好。

\\

单元测试和随心所欲的表达式

\\

您仍然需要单元测试来测试你的程序逻辑。但相比动态类型语言,你不需要那么多针对类型的琐碎单元测试。根据很多人的经验,Scala所需的单元测试比动态语言少得多。你的经历可能有所不同,但我们在大量案例中所得体验的确如此。

\\

另一条针对静态类型系统的反对意见是:静态类型系统对表达方式限制太严。人们说,“我想自由地表达自己。我不想要静态类型系统的条条框框”。根据我的Scala经验,这种意见不靠谱,我认为有两个原因。第一个原因是,Scala的类型系统实际上非常灵活,所以通常它可以让你用非常灵活的模式排列组合。反之,像Java这样的语言,类型系统表达能力偏弱,就难以实现。第二个原因是,通过模式匹配,你可以通过非常灵活的方式抽回类型信息,甚至根本感觉不到类型信息的损失。

\\

Scala模式匹配的亮点在于,我可以对我一无所知的对象,用类似switch语句的结构,提供若干模式,进行匹配。只要这些模式之一匹配成功,我还能够立刻取出其中的字段,存到局部变量上。模式匹配是Scala核心中内置的结构。许多Scala程序都用了它。这属于用Scala干活的日常惯例。模式匹配还有个有趣的功能:自动抽回类型。当对你不知道类型的对象进行模式匹配时,如果匹配成功,实际上模式本身其实就可以提供一些类型信息。而类型系统可以利用这些信息。

\\

有了模式匹配,你可以很容易拥有一套系统,在系统中,你只使用通用类型(甚至通用到了极致,所有变量都是Object),但你仍然可以靠使用模式匹配获得所有类型信息。因此,在这个意义上,在Scala程序中,你可以像动态类型语言一样编写完美的动态代码。你可以到处都用Object,只要到处都模式匹配即可。现在一般人不这样做,是为了更好的利用静态类型的优势。但这种做法算是一种非常平滑的备用方案,平滑到了不知不觉的程度。相比之下,在Java中,类似情况下,你必须使用大量的类型检测(instanceof)和类型转换,可谓是又笨又重。我当然完全理解人们为什么反对到处滥用这种写法。

\\

聒噪的鸭子

\\

Bill Venners: 我观察到一件有关Scala的事情,相比Java的类型系统,在Scala类型系统中,我可以让程序表达出更多东西。从Java逃向动态语言的人往往会解释说,他们对类型系统感到沮丧,扔掉静态类型后他们感觉更好了。然而Scala似乎给出了另一个答案:尝试去改善类型系统,让它用途更广,用着更爽。哪些事情是在Scala类型系统能做到但在Java类型系统中却做不到?

\\

Martin Odersky: 针对Java类型系统有一条反对意见:缺了所谓的鸭子类型。可以这样解释鸭子类型:“如果它走起来像鸭子,叫起来像鸭子,那么它就是鸭子。”翻译一下:只要它具备我所需的功能,那么就可以把它当真。举例来说,我希望取得某种支持关闭操作的资源。我希望以这种方式声明:“它必须有close方法。”我不在乎它是File、Channel还是其他任何东西。

\\

要想在Java中实现的话,你需要定义一个包含该方法的通用接口,而大家都需要实现这个接口。首先,为了实现这一切,将导致大量接口和大量样板代码。其次,如果有了既成事实之后,你才想到要提个接口,那么几乎就不可能做到。如果事先就写了一些类,鉴于这些类早已存在,那么你不改代码就没法加入新接口,除非修改所有客户代码。所以,这一切困难都是类型强加给你的限制。

\\

另一方面,Scala比Java表现力更强。Scala能表达鸭子类型。你完全可以用Scala把某一类型定义为:“任何拥有close方法且close返回Unit(类似Java的void)的类型”。你还可以把类型定义与其他约束结合起来。你可以这样定义类型:任何继承自某个类型,且拥有某某方法签名的类型。你还可以这样定义:任何继承自某某类,且拥有某某类型的内部类的类型。从本质上讲,你可以通过定义类型中有哪些东西将为你所用,来描绘类型的结构。

\\

既存类型(existential types)

\\

Bill Venners: 既存类型加入Scala的时间比较晚。我听说Scala加入既存类型的理由是,为了把所有Java类型映射到Scala中。具体到既存类型,可以对应到Java的通配符类型。既存类型是否比通配符更强大?是不是Java通配符类型的超集?还有哪些理由要告诉大家?

\\

Martin Odersky: 不好说。因为大家对通配符并没有真正靠谱的构想。原先的通配符由Atsushi Igarashi和Mirko Viroli设计。他们的灵感来自既存类型。实际上原先的论文中的确包含了既存类型的字节码编码方案。但后来当实际最终设计加进Java时,二者的联系有所削弱。所以,我们也不知道通配符类型的确切现状了。

\\

既存类型早已发明,有着距今约20年的历史。既存类型要表达的概念很简单。它表示,给定某种类型,比如List,但其内部元素是未知类型。你只知道它是由某种特定类型元素组成的List,然而你并不知道元素的“特定类型”具体是哪种类型。在Scala中,这种概念可以用既存类型来表达。语法如下:List[T] forSome { type T }。稍微有点笨重。笨重的语法其实算是有意为之。因为事实证明,既存类型往往不大好处理。Scala有更好的选择。Scala不是那么需要既存类型,因为Scala的类型可以包含其它类型作为内部成员。

\\

归根结底,Scala只有三种情况需要既存类型。第一,Scala需要能表示Java通配符的语义。既存类型能提供这种语义。第二,Scala需要能表示Java的raw类型,因为有许多库使用了非泛型类型,会出现raw类型。如果有一个Java raw类型(如 java.util.List),那么它其实是未知元素类型的List。它也可以用Scala中的既存类型表示。第三,既存类型可以用来把虚拟机中的实现细节反映到上层。类似Java,Scala使用的泛型模型是“擦除模型”。所以在程序运行起来以后,我们就再也找不着类型参数了。之所以要进行擦除,是为了与Java的对象模型可以互相操作。可是,如果我们需要反射,或者要表达虚拟机的实现细节时,怎么办?我们需要有能力用Scala中的某些类型表示Java虚拟机的行为。既存类型就提供了这种能力。有了既存类型,即使某一类型中的某些方面尚不可知,我们仍然可以操作该类型。

\\

Bill Venners: 你能举个具体例子吗?

\\

Martin Odersky: 以Scala的List为例。我希望能够描述head方法的返回类型 。该方法返回List的第一个元素(即首元素)。在虚拟机级别,List类型是List[T] forSome { type T }。我们不知道T是什么,只知道head返回T 。既存类型理论告诉我们,该类型是“某些类型T中的T”,也就相当于根类型, Object 。那么我们从head方法取回的就是Object。因此,在Scala中,要是我们知道更多信息,我们可以直接指定具体类型而不用既存类型的限定规则。但要是没有更多信息,我们就留着既存类型,让既存类型理论帮我们推断出返回类型。

\\

Bill Venners: 如果当初你不需要考虑Java兼容性,比如通配符类型、raw类型和类型擦除,那么你还会加入既存类型吗?如果Java采用的是具现化的泛型类型系统,不支持raw类型或通配符类型,那么Scala还会有既存类型吗?

\\

Martin Odersky: 如果Java采用的是具现化的泛型类型系统,不支持raw类型或通配符类型,那么我觉得既存类型用处不大,恐怕Scala中不会加入。

\\

Java和Scala中的型变(Variance)

\\

Bill Venners: Scala中的型变定义位于类型定义之处,而Java的型变则要定义在使用通配符的代码之处。你能否谈谈二者的差异?

\\

Martin Odersky: Scala的既存类型一样支持通配符,所以,只要你愿意,你照样可以在Scala中使用与Java相同的写法。但是,我们建议你不要这样做,我们鼓励你改用位于类型定义之处的型变语法。为什么呢?首先,什么是“类型定义之处的型变”?当你定义某个类的类型参数时,例如List[T]时,会有一个问题。如果给你一个“苹果(Apple)列表”,那么,它算不算“水果(Fruit)列表”呢?你会说,当然算。只要Apple是Fruit的子类型, List[Apple]就应该是List[Fruit]子类型。这种子类型的关系称为协变(covariance) 。但在某些情况下,这种关系不成立。比方说,我有一个变量,只能用来保存Apple,那么这个变量就是对类型Apple的引用。这个变量并不能当做Fruit类型的引用 ,因为我并不能把任意Fruit赋值给这个变量。它只能是Apple。所以,你可以看到,上述的子类型关系,在有一些情况下适用,另一些情况下不适用。

\\

Scala中的解决方案是,给类型参数加个标志。如果List中的T支持协变 ,我们可以写做List[+T]。这将意味着任意List之间的关系都可以随着其T的关系而协变。要想支持协变,必须遵守协变的附加条件。举例来说,只有List内容不可变时,List才能支持协变,因为若非如此,就会遇上刚才引用变量例子中类似的问题,而导致无法协变。

\\

Scala中的机制是这样的:程序员可以声明“我觉得List应该支持协变”,即,要求List必须遵守子类型关系。那么,程序员把在类型声明之处,给类型参数T标上加号——只标注一次。而List的任何用户,都只需直接使用即可。然后,编译器会去找出List内的所有定义实际上是否兼容于协变,确保List中不存在导致冲突的成员签名。如果Scala编译器发现了不兼容协变之处,就触发编译错误。Scala社区有一系列的惯用技术来解决这些错误。称职的Scala程序员通常可以很快掌握这些技术。当称职的Scala程序员用上这些技术时,只要他编写的类最终通过了编译,就能为用户提供协变性。而用户就不再需要考虑协变问题了。用户只知道,只要给定一个List,就能以协变方式到处使用了。因此,这意味着,仅仅只有编写List类的那一个人必须多思考一点。但这其实不算太难,因为编译器会输出错误信息来帮助他。

\\

相比之下,以Java的方式使用通配符,这就意味着库的提供者对协变爱莫能助,只能草草定义成List\u0026lt;T\u0026gt;了事。但接下来如果用户需要协变List,却不能写做List\u0026lt;Fruit\u0026gt;而必须写做List\u0026lt;? extends Fruit\u0026gt;。通配符就是这样用的。问题在于,这是用户代码啊!用户总不可能人人都像设计库的人那么专业吧。此外,这些标注之间,只要有一处不匹配,就会导致类型错误。就算通配符搞出了海量极晦涩的错误信息,那也毫不称奇。我觉得这是Java泛型为人诟病的首要原因了。因为,通配符用起来实在是相当复杂,正常人类根本无从把握、无法处理。

\\

当我们结合泛型和子类型时,型变是个核心功能。然而它也很复杂。并没有什么办法可以完全化解其复杂度。我们能比Java做得好点,就在于,我们让你可以在库中处理型变,使用户感觉不到型变存在,不必手动处理型变。

\\

抽象类型成员

\\

Bill Venners: 在Scala中,一个类型可以是另一种类型的内部成员,正如方法和字段可以是类型的内部成员。而且,Scala中的这些类型成员可以是抽象成员,就像Java方法那样抽象。那么抽象类型成员和泛型参数不就成了重复功能吗?为什么Scala两个功能都支持?抽象类型,相比泛型,能额外给你们带来什么好处?

\\

Martin Odersky: 抽象类型,相比泛型,的确有些额外好处。不过还是让我先说几句通用原理吧。对于抽象,业界一直有两套不同机制:参数化和抽象成员。Java也一样支持两套抽象,只不过Java的两套抽象取决于对什么进行抽象。Java支持抽象方法,但不支持把方法作为参数;Java不支持抽象字段,但支持把值作为参数;Java不支持抽象类型成员,但支持把类型作为参数。所以,在Java中,三者都可以抽象。但是对三者进行抽象时,原理有所区别。所以,你可以批判Java,三者区别太过武断。

\\

我们在Scala中,试图把这些抽象支持得更完备、更正交。我们决定对上述三类成员都采用相同的构造原理。所以,你既可以使用抽象字段,也可以使用值参数;既可以把方法(即“函数”)作为参数,也可以声明抽象方法;既可以指定类型参数也可以声明抽象类型。总之,我们找到了三者的统一概念,可以按某一类成员的相同用法来使用另一类成员。至少在原则上,我们可以用同一种面向对象抽象成员的形式,表达全部三类参数。因此,在某种意义上可以说Scala是一种更正交、更完备的语言。

\\

现在的问题来了,这对你有什么好处?具体到抽象类型,能带来的好处是,它能很好地处理我们先前谈到的协变问题。举个老生常谈的例子:动物和食物的问题。这道题是这样的:从前有个Animal类,其中有个eat方法,可以用来吃东西。问题是,如果从Animal派生出一个类,比如Cow,那么就只能吃某一种食物,比如Grass。Cow不可以吃Fish之类的其他食物。你希望有办法可以声明,Cow拥有一个eat方法,且该方法只能用来吃Grass,而不能吃其他东西。实际上,这个需求在Java中实现不了,因为你最终一定会构造出有矛盾的情形,类似我先前讲过的把Fruit赋值给Apple一样。

\\

请问你该怎么做?Scala的答案是,在Animal类中增加一个抽象类型成员。比方说,Scala版的Animal类内部可以声明一个SuitableFood类型,但不定义它具体是什么。那么这就是抽象类型。你不给出类型实现,直接让Animal的eat方法吃下SuitableFood即可。然后,在Cow中声明:“好!这是一只Cow,派生自Animal。对Cow来说,其SuitableFood是Grass。”所以,抽象类型提供了一种机制:先在父类中声明未知类型,稍后再在子类中填上某种已知类型。

\\

现在你可能会说,哎呀,我用参数也可以实现同样功能。确实可以。你可以给Animal增加参数,表示它能吃的食物。但实践中,当你需要支持许多不同功能时,就会导致参数爆炸。而且通常情况下,更要命的问题是,参数的边界。在1998年的ECOOP(欧洲面向对象编程会议)上,我和Kim Bruce、Phil Wadler发表了一篇论文。我们证明,当你线性增加未知概念数量时,一般来说程序规模会呈二次方增长。所以,我们有了很好的理由不用参数而用抽象成员,即为了避免二次方级别的代码膨胀。

\\

用惯Scala的语法

\\

Bill Venners: 大家随便翻些Scala代码来读时,会有两件事情,让大家觉得Scala有点隐晦。首先,可能会遇上某种不熟悉的DSL(领域特定语言),比如Scala的parser combinators库或是XML库。其次,可能会遇上类型系统中的各种怪符号,尤其当这些怪符号一起出现时。Scala程序员怎么才能找到处理这类语法的诀窍呢。

\\

Martin Odersky: 当然,需要学习和吸收的新东西不少。因此,这需要一定的时间。我相信有一件事我们必须努力:更好的工具支持。目前,如果你触发了了类型错误,我们尽量给你提供友好的错误信息。有时,为了能解释为何出错,错误信息会横跨多行。我们一直在努力,但我觉得我们还可以做得更好:我们可以增加错误信息的交互性。

\\

试想一下,假如你用动态类型语言时发生了运行时错误,每条错误信息最多三四行。而且又没有调试器、没有调用栈信息,就只有三四行的“未将对象引用设置到对象的实例”,可能最多再给你一个行号。那么这种情况下,我觉得动态类型语言不可能流行起来。当然啦,现实中的动态类型语言没这么弱,它会把你扔到调试器中,让你可以快速找出错误根源。

\\

我们的类型系统目前还做不到。我们只能得到这点错误信息。Scala的类型系统非常丰富、表达力强,需要更多知识才能让错误信息靠谱,程序员就会需要更多工具的协助。所以,我们今后会调研一件事,我们能不能真正提供更具交互性的环境,让程序员能在类型系统出错时找出错误原因。例如,如何让编译器能找出表达式的类型、能知道为什么实际类型与所需类型没匹配上。我们可以通过交互方式挖出这些信息。我想,到了那时,程序员就可以比现在更容易找到类型错误的原因了。

\\

另一方面,一些语法还很新,需要一些时间适应。这一点我们可能无法回避。我们只希望若干年后,这些语法能被大家不假思索、完全自然的接受。目前主流语言中的一些东西,当初大家接受时,也花了不少时间。我清楚的记得,异常刚出现时,大家也觉得很奇怪,花了很多时间才适应。而到了现在,每个人都觉得异常用起来相当自然了,不再觉得新奇。Scala确实引入了一些新东西(主要是在类型方面),这些新东西需要一些时间来适应。

\\

查看英文原文:The Purpose of Scala's Type System

\\


感谢魏星对本文的审校。

\\

给InfoQ中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家通过新浪微博(@InfoQ,@丁晓昀),微信(微信号:InfoQChina)关注我们,并与我们的编辑和其他读者朋友交流(欢迎加入InfoQ读者交流群)。

Scala类型系统的目的——Martin Odersky访谈(三)相关推荐

  1. Scala模式匹配的亮点——Martin Odersky访谈(四)

    Martin Odersky向Bill Venners和Frank Sommers谈论Scala模式匹配的机制和目的. \\ Scala是一种新兴的通用用途.类型安全的Java平台语言,结合了面向对象 ...

  2. Scala的设计目标——Martin Odersky访谈(二)

    Scala是一种新兴的通用用途.类型安全的Java平台语言,结合了面向对象和函数式编程.它是洛桑联邦理工大学教授Martin Odersky的心血结晶.本访谈系列由多部分组成,由Artima网站的Fr ...

  3. Martin Odersky Scala编程公开课 第三周作业

    Functional Programming Principles in Scala  by Martin Odersky 这次的作业叫做Object-Oriented Sets.要完成一个完整的类, ...

  4. Martin Odersky Scala编程公开课 第二周作业

    Functional Programming Principles in Scala  by Martin Odersky 这一周的主要内容是函数.函数是scala语言最重要的概念,既可以当作函数的参 ...

  5. Martin Odersky Scala编程公开课 第一周作业

    Functional Programming Principles in Scala  by Martin Odersky Martin教授是scala语言的creator,在coursera上面有s ...

  6. She Knows访谈 | 三大公链创始人技术硬核交锋,2019年浪潮将由什么引爆?

    She Knows第三期对话嘉宾:本体(Ontology)创始人李俊网录科技创始人.万维链(Wanchain)发起人吕旭军Nervos基金会联合创始人吕国宁. 对话人:巴比特内容总监王晓萌,资深媒体人 ...

  7. Scala类型系统——高级类类型(higher-kinded types)

    高级类类型就是使用其他类型构造成为一个新的类型,因此也称为 类型构造器(type constructors).它的语法和高阶函数(higher-order functions)相似,高阶函数就是将其它 ...

  8. 技术人攻略访谈三十七-程显峰:IT病得有多重?技术圈交际花谈研发管理怪现状

    文:Gracia (本文为原创内容,部分或全文转载均需经过作者授权,并保留完整的作者信息和技术人攻略介绍.) 导语:本期采访对象程显峰@程显峰-Mars,蓝海讯通COO.素有"技术圈交际花兼 ...

  9. 技术人攻略访谈三十八-许式伟:十一年逆流顺流,首席架构师到CEO

    文:Gracia (本文为原创内容,部分或全文转载均需经过作者授权,并保留完整的作者信息和技术人攻略介绍.) 导语:本期访谈对象@许式伟,七牛云存储CEO,国内Go语言圈领军人物,ECUG社区发起人. ...

最新文章

  1. 网站性能越来越差怎么办?
  2. Python网络爬虫 - 一个简单的爬虫例子
  3. Bio+IT 爱好者社区,欢迎你!
  4. 系统服务器巡查表,服务器操作系统巡检表
  5. 外贸网站建设需要考虑的五大层面
  6. 机器学习-损失函数 (转)
  7. mysql事务 可见性,【每日阅读】2020年12月09日-事务先后的可见性
  8. [剑指offer][JAVA]面试题第[16]题[数值的整数次方][位运算][二分法]
  9. LeetCode 旋转数组 系列
  10. 安装kenlm出现问题的解决方案gcc g++
  11. 【算法】剑指 Offer 45. 把数组排成最小的数 【重刷】
  12. python.集合转列表_python 列表,元组,字典,集合,字符串相互转换
  13. Java序列化机制和原理
  14. 配置百度云CDN加速
  15. cocos creator 打包apk_Cocos Creator Android打包 apk
  16. Ubuntu系统文件被上锁了怎么编辑:Ubuntu系统获得读写权限
  17. 重启计算机可以使用什么组合键,死机重启电脑快捷键有哪些
  18. Typora中写论文怎么添加reference(参考文献)
  19. 如何用计算机校验信息,Win10如何校验文件哈希值(系统自带方法)?
  20. 单片空间后方交会 python实现

热门文章

  1. mxnet img2rec的使用,生成数据文件
  2. c# picturebox控件显示本地图片和显示网上的图片
  3. 分享几个益智题......看你能做对吗?
  4. python 多个列表合并_Python实现合并两个列表的方法分析
  5. AOP五大通知注解详解
  6. 无需担心架构演变 入云的Teradata无处不在
  7. Web表单美化CSS框架Topcoat
  8. Ubuntu安装过程中的问题
  9. Oracle在JavaOne上宣布Java EE 8将会延期至2017年底
  10. 有了bootstrap,为什么还要做amaze ui