当前位置:首页 >科教 >面向对象编程已死,OOP 永存!

面向对象编程已死,OOP 永存!

2019-12-08 10:07:06 [游泳队] 来源:郁金网
原始链接:https://data.newrank.cn/m/s.html? S = NykqPTa9Li5j[编辑关于程序生命的注释] ECS(ECS ,实体-组件-系统 ,实体组件系统,主要用于游戏开发的架构模式)是在游戏开发社区广泛流传的伪模式。它基本上是关系模型的复制品,其中“实体”是标识 ,表示不可见的对象,“组件”是特定表中的一行,引用标识,“系统”是用于更改组件的过程代码 这种“模式”经常导致继承的过度使用 ,而没有提到继承的过度使用,这实际上违反了面向对象编程(面向对象编程,面向对象编程,计算机编程体系结构)的原则。 那么如何避免这种情况呢?本文作者将向您介绍真正的设计指南。 作者|布鲁克·霍奇曼翻译家|弯月,编辑|胡炜制作| CSDN(身份证:CSDN新闻)灵感本文的灵感来自联合的著名工程师阿拉斯·普兰克维奇(Aras Pranckevič ius)最近为初级开发者所做的公开演讲。演讲的目的是让他们熟悉新的“ECS”体系结构的一些术语。 Aras使用非常典型的模式。他展示了一些非常糟糕的面向对象程序代码,然后指出关系模型是一个更好的方案(尽管这里的关系模型被称为“ECS”) 我不想批评阿拉斯。事实上,我非常喜欢他的作品,也非常欣赏他的演讲 。我选择他的演讲而不是ECS在线上的数百篇其他文章的原因是,他的演讲给出了代码,并且有一个非常简单的小“游戏”来演示各种架构 。 这个小项目节省了我很多精力,也让我很容易表达自己的观点。所以,谢谢阿拉斯!Aras幻灯片链接:http://ARAS-p.info/texts/files/2018学院% 20-%20ECS-DOD.pdf代码链接:https://github.com/aras-p/dod-playground我不想分析他演讲结束时提出的ECS架构,只想谈谈我对他批评的“糟糕的面向对象”代码的看法。 我想讨论的是如果我们能够纠正所有违反面向对象设计原则的行为会发生什么 。 扰流板警告:纠正违反OOD的代码可以实现与Aras的ECS版本类似的性能提升,并且它还可以比ECS版本占用更少的内存和代码量!总之,如果你认为面向对象是垃圾,而环境控制系统是国王,那么首先要了解面向对象(即如何正确使用面向对象) ,然后再学习关系模型(知道如何正确使用环境控制系统) 我一直讨厌论坛上许多关于ECS的帖子,部分是因为我认为ECS不能得到一个单独的术语(破坏者:它只是关系模型的一个特殊版本),部分是因为所有推广ECS模式的帖子、幻灯片或文章都有相同的结构:显示一些非常糟糕的面向对象程序代码 ,它们的设计非常垃圾,而且它们通常过度使用继承(这一个违反了许多面向对象的原则) 证明组合比继承更好(伍德早就说过) 证明关系模型非常适合游戏开发(只改名为ECS) 这种结构的文章让我很恼火,因为:秘密地改变概念 很难让人相信它所比较的对象是完全不同的。尽管这可能是无意的,但并不能证明它提出的新架构更好。 它会产生副作用,贬低知识,无意中打击读者学习该领域50多年的研究成果。 关系模型最早是在20世纪60年代提出的。 在20世纪70年代和80年代,对该模型的各个方面进行了深入研究。 新手经常问的问题是“这些数据应该放在哪个类别 ?”这个问题的答案通常很模糊,“当你有更多的经验时,你自然会知道。” 然而,在70年代,这个问题被彻底地研究,并以一种普通和正式的方式解决,即数据库的标准化(https://en.wikipedia.org/wiki/database标准化#范式) 忽视现有的研究成果,把ECS作为一个全新的方案来展示,就等于隐藏知识,不告诉程序员新手 。 面向对象编程也有很长的历史(实际上比关系模型更长 ,它的概念从20世纪50年代就出现了)!然而,面向对象直到20世纪90年代才引起人们的注意,并成为主流编程范式。 各种面向对象语言涌现出来 ,包括Java和(标准版本)C++ 然而,因为它被大肆宣传,每个人都只是在简历上写了这个词,很少有人真正知道。 这些新语言引入了许多关键字来实现面向对象功能,例如类、虚拟、扩展 、实现。从那以后,我认为面向对象被分成了两个组。 后来,我把具有面向对象思想的编程语言称为“面向对象”,把使用面向对象思想的设计和架构技术称为“面向对象” 每个人都很快学会面向对象程序设计 ,学校还说面向对象课程非常有效,适合新手程序员...然而,OOD知识被抛在了后面。 在我看来 ,使用面向对象语言特性但不遵循面向对象设计规则的代码不是面向对象代码 大多数反面向对象的文章攻击的代码不是真正的面向对象代码 。 面向对象程序代码名声不好,部分原因是大多数面向对象程序代码不遵循面向对象的原则,所以它们不是真正的面向对象程序代码。 背景如前所述,20世纪90年代是面向对象大爆炸的时代,那个时期的“糟糕的面向对象代码”可能是最糟糕的。 如果你当时学习了面向对象程序设计,那么你可能学到了以下“面向对象程序设计的四个支柱”:抽象封装多态性继承我更喜欢称它们为“面向对象程序设计的四个工具”,而不是四个支柱 这些工具可以用来解决问题 然而,仅仅学习工具的使用是不够的。你必须知道何时使用它们。 教育工作者只教工具的使用而不教工具的使用场景是不负责任的。 21世纪初,出现了第二次面向对象的思想浪潮,工具的滥用受到了一定程度的抑制。 当时,固体(https://en.wikipedia.org/wiki/SOLID)思想体系被提出来快速评价设计质量。 请注意,这些建议实际上在20世纪90年代广为流传 ,但当时没有像“固体”这样简单而难忘的词来将其转化为五项核心原则...单一反应责任原则 每个班级应该只有一个目的 如果甲类有两个目的,那么乙类和丙类分别创建来处理每个目的 ,然后甲类从乙类和丙类中提炼出来 开放/封闭原则 软件总是在变化(即维护很重要) 将可能改变的部分放入实现中(即具体类),并为不太可能改变的东西建立接口(例如抽象基类)。 利斯科夫替代原则 每个接口的实现都应该100%符合接口的要求,也就是说,可以在接口上运行的任何算法都应该能够在特定的实现上运行。 界面分离原则 接口应该尽可能小,以确保代码的每个部分“只需要知道”最少量的代码,也就是说 ,避免不必要的依赖。 这个建议对C++也非常有用,因为不遵循这个原则会大大增加编译时间。 依赖倒置原则 实现直接通信并相互依赖的两种特定模式可以通过将两者之间的通信接口标准化为第三类来解耦,第三类充当两者之间的接口 。 第三类可以是定义两者之间所需调用的抽象累积,甚至是定义两者之间传输的数据的简单数据结构。 这个不在SOLE中,但是我认为这个同样重要:复合重用原则 默认情况下应该使用组合,只有在必要时才应该使用继承 这是我们的SOLID C++ 接下来,我将使用三个字母的缩写来代表这些原则:SRP、OCP、LSP、ISP、DIP、CRP 另一个观点是:在面向对象中,接口和实现不对应于任何特定的面向对象关键字 在C++中,接口通常用抽象类和虚拟函数构建,然后从基类继承……但这只是实现接口概念的一种方式。 PIMPL(https://en . cppreference . com/w/CPP/language/pimpl)、不透明指针(https://en . Wikipedia . org/Wiki/Opaque _ pointer)、鸭子类型(https://en.wikipedia.org/)可用于C++ Wiki/Duck_typing 、typedef等……您甚至可以创建一个OOD设计,并使用完全不支持OOP关键字的C语言来实现它!因此,我在这里指的界面不一定是一个虚函数,而是一个隐藏的实现想法(https://en.wikipedia.org/wiki/Information_hiding) 界面可以是多态的(https://en.wikipedia.org/wiki/polymorphism _(计算机科学)),但大多数情况下不是!良好的多态性非常罕见,但是任何软件都将使用接口。 如上所述,如果建立一个简单的数据结构将数据从一个类传输到另一个类,那么这个结构就像一个接口——在形式语言中,这被称为数据定义(https://en.wikipedia.org/wiki/Data_definition_language) 即使只有一个类被分成公共部分和私有部分,那么公共部分中的一切都是接口,私有部分中的一切都是实现。 继承实际上(至少)有两种类型:接口继承和实现继承 在C++中,接口继承包括抽象基类、PIMPL和由纯虚函数实现的条件类型定义 在Java中,接口继承由implements关键字表示 在C++中,当所有基类都包含纯虚函数以外的内容时,就会发生实现继承 在Java中 ,实现继承由extensions关键字表示 OOD定义了许多关于接口继承的规则,但是实现继承通常是一个不祥的预兆(https://en.wikipedia.org/wiki/Code_smell) 最后,我也许应该举出一些不好的面向对象程序教育的例子,以及由这种教育造成的不好的代码(和面向对象程序的坏名声)。 在研究等级和继承时,你可能已经学到了以下类似的例子:假设我们有一个学校应用程序,其中包括学生和教职员工的目录。 因此 ,我们可以使用Person作为基类,然后从Person继承学生和员工类 。 这是完全错误的 等一下 里克特替换原理指出类的层次和操作它们的算法是共生的 它们是一个完整程序的两个部分。 面向对象编程是过程编程的扩展。它的主要结构仍然是一个过程。 因此,如果您不知道学生和教职员工的算法(以及哪些算法可以通过多态性简化),那么设计类层次结构是不负责任的 您必须有算法和数据才能继续 。 在学习层次和继承时 ,您可能已经学习了以下类似的例子:假设您有一类形状 它的子类可以有正方形和长方形 那么,它应该是正方形是-长方形还是长方形是-正方形?这个例子实际上很好地展示了实现继承和接口继承之间的区别。 如果您正在考虑实现继承,那么您根本就没有考虑LSP,只是使用继承作为重用代码的工具。 从这个角度来看,以下定义是完全合理的:结构正方形{ int width};结构矩形:正方形{中间高度;};正方形只有一个宽度,而矩形除了宽度之外还有一个高度,所以如果你用一个高度来扩展正方形 ,你可以得到一个矩形!你一定猜到OOD认为这个设计(可能)是错误的 。 我说可能的原因是你仍然可以争论隐含的接口...但是没关系 正方形的宽度和高度总是相同的,所以从正方形界面的角度来看,我们可以把它的面积看作“宽×宽” 如果矩形继承自正方形,那么根据LSP ,矩形必须遵循正方形接口的规则 所有在正方形上正确工作的算法必须在矩形上正确工作 。 例如,以下算法:std::vector<。正方形* >;形状;int area = 0;对于(自动s:形状)区域+= s->;宽度* s->;宽度;该算法在正方形(产生所有面积的总和)上正确工作,但在矩形上不正确。 因此 ,矩形违反了最小二乘原理 如果从接口继承的角度考虑,正方形和矩形都不应该互相继承。 正方形和长方形之间的界面实际上是不同的,没有人是任何人的超集。 因此,OOD实际上并不鼓励继承 如前所述,如果你想重用代码,OOD认为你应该使用组合!因此,实现上述继承的层次结构代码的正确版本应该用C++编写:StructShape { VirtualInStream()常量= 0;};结构广场:公共虚拟形状{虚拟中间区域()常数{返回宽度*宽度;};int宽度;};结构矩形:私有正方形,公共虚拟形状{虚拟int area()常数{ return width * height};int高度;};公共虚拟等同于在Java中实现,并且在实现接口时使用 Private允许您从基类继承,而不必继承它的接口。 在本例中,矩形不是正方形,尽管它继承了正方形 。 我不建议编写这样的代码,但是如果您真的想使用实现继承,那么这是编写代码的正确方法!总之 ,面向对象的课程教你什么是继承,而你没有学的面向对象的课程应该教你99%的时候不要使用继承!有了实体/组件框架中的这些背景,让我们看看Aras开头提到的所谓的“公共面向对象程序”。 事实上,我还想说Aras称这些代码为“传统面向对象程序”,但我不这么认为。 这些代码可能是常用的面向对象程序,但是如上所述,这些代码打破了所有的核心面向对象规则,所以它们根本不是传统的面向对象程序。 我们从最早的提交开始——当时他没有将设计更改为ECS:“让它在windows上再次工作”(http://git hub . com/aras-p/DOD-player/blob/3529 f 232510 c 95 f 53112 bbf ff 87 df 6 BBC 6a a 1 FAE/source/game . CPP):class game object;类别组件;typedef std::vector<。组件* >;分量向量;typedef std::vector< 。游戏对象* >;游戏对象向量;类组件{公共:组件():m_GameObject(nullptr) {}虚拟~组件(){}虚拟空开始(){}虚拟空更新(双倍时间,浮动增量时间){}常量GameObject & ampGetGameObject()常量{ return * m _ GameObject}游戏对象和;getGameObject(){ return * m _ GameObject;}作废SetGameObject(游戏对象和;go){ m _ GameObject = & amp;去吧。} bool HasGameObject()常量{返回m_GameObject!= nullptr}私人:游戏对象* m _游戏对象;};类GameObject { public:GameObject(const STD::string & amp;& amp名称):m_Name(名称){ } ~GameObject() { for(自动c : m_Components)删除c;}模板<。键入名称。T * GetComPonents(){用于(自动i : m_Components) { T* c =动态_铸造& ltT* >;㈠;如果(c!= nullptr)返回c;}返回nullptr}无效添加组件(组件* c) {断言(!c->;HasGameObject());c->;设置游戏对象(*这个);m _组件.就位_返回(c);}取消开始(){对于(自动c:m _组件)c->;start();}无效更新(双倍时间,浮动增量8000me) {用于(自动c:m _组件)c->;更新(时间,增量时间);}私有:标准::字符串m _ Name分量向量m _分量;};静态游戏对象矢量对象;模板<。键入名称。静态组件向量查找所有组件软件类型(){组件向量资源;对于(自动go:s _ Objects){ T * c = go->;GetComponent<。T>。();如果(c!= null ptr)RES . present _ back(c);}返回res}模板< 。键入名称。静态测试*查找类型(){用于(自动执行:s _对象){测试* c =执行->;GetComponent<。T>。();如果(c!= nullptr)返回c;}返回nullptr}好吧,代码很难马上理解,所以让我们来分析一下...但是还有另一个背景:在20世纪90年代,继承被用来解决所有代码重用问题 ,这在游戏行业是一种常见的做法。 首先有一个实体,然后扩展到角色,然后扩展到玩家 ,怪物,等等...如前所述,这是为了实现继承。虽然一开始看起来不错,但最终会导致极其不灵活的代码。 因此,OOD有“用组合代替继承”的规则 因此,游戏开发只是在本世纪初“用组合代替继承”的规则流行之后才开始编写这段代码。 这段代码实现了什么?总的来说 ,这不好,呵呵 简而言之,这段代码通过运行时函数库再次实现了组合功能,而不是使用语言特性来实现它。 您可以认为这段代码在C++和运行这种语言的编译器之上构建了一种新的语言。 Aras的示例游戏中没有使用此代码(我们将在一会儿全部删除!),其唯一目的是将游戏性能降低10倍。 它到底做了什么?这是一个“实体/组件”框架(有时被错误地称为“实体/组件系统”),但它与“实体组件系统”框架无关(后者显然不会被称为“实体组件系统”) 游戏从一个不起作用的“实体”(本例中称为游戏对象)开始,它本身由“组件”组成 游戏对象实现了服务定位器模式,它可以按类型查询子组件 组件知道它属于哪个游戏对象,并且它们可以通过查询父游戏对象来定位兄弟组件。 组合仅限于单个层(组件不能有子组件,游戏对象不能有子组件) 游戏对象每种类型只能有一个组件(有些框架需要 ,有些不需要) 所有组件(可能)都以未知的方式变化,因此接口被定义为“虚拟无效更新” 游戏对象属于一个场景,这个场景可以查询所有的游戏对象(所以你可以继续查询所有的组件) 这个框架在本世纪初非常流行 。尽管它非常严格,但它提供了足够的灵活性来支持无数的游戏,现在仍然如此。 然而,这样一个框架是不必要的 编程语言的特性中已经提供了组合,并且没有必要用框架来再次实现它们……那么为什么需要这些框架呢 ?这是因为框架可以实现动态的运行时组合。 游戏对象可以从数据文件中加载,无需硬编码。 这样,游戏设计者和关卡设计者可以创建他们自己的对象...然而,在大多数游戏项目中,设计师很少,程序员很多,所以我不认为这是关键功能。 此外 ,还有许多其他方法来实现运行时合成!例如 ,Unity使用C#作为它的“脚本语言”,许多其他游戏使用替代语言,例如Lua,所以面向设计者的工具可以生成C#/Lua代码来定义没有这些框架的新游戏对象!我们将在以后的文章中再次添加运行时组合的“函数”,但同时避免10倍的性能开销...如果我们从面向对象的角度来评估这段代码:游戏游戏对象:GetComponent使用动态造型 大多数人会告诉你动态造型是一种代码味道——它强烈地表明代码有什么问题。 在我看来,这表明您的代码违反了LSP——一个算法正在操作基类的解耦,但是它需要理解不同实现的细节。 这就是代码闻起来不好的原因。 如果考虑实现服务定位器模式,游戏对象是可以的...但是从OOD的角度来看,这种模式在项目的不同部分之间建立了一种隐含的联系,我认为(我找不到一个维基链接可以用计算机科学知识来支持我)这种隐含的沟通渠道是一种消极的模式(Https://en . Wikipedia . org/wiki/Anti-pattern),应该使用一种明确的沟通渠道。 这个观点也适用于一些游戏中使用的“事件框架”……我认为组件违反了SRP(单一责任原则) ,因为它的接口(虚拟空更新(时间))太宽了。 “虚拟空洞更新”在游戏开发中非常常见,但我仍然想说它是一个反向模型。 好的软件应该很容易演示它的控制流程和数据流 将所有游戏代码放在“虚拟空更新”调用之后,会完全混淆控制流和数据流。 在我看来,不可见的副作用(https://en.wikipedia.org/wiki/side效应_(计算机科学))-也被称为“远距离效应”(https://en.wikipedia.org/wiki/行动_ at _ a _ distance _(计算机编程)-是最常见的bug来源,“虚拟空洞更新”使一切都有不可见的副作用 虽然组件类的目的是实现组合,但是它是通过继承来实现的 ,这违反了CRP(组合重用的原则) 该代码的好处是它满足SRP和ISP(接口隔离原则),并划分了大量简单的组件。每个组件几乎没有责任,这非常适合代码重用。 然而,它在依赖反转原理(DIP)中表现不佳,并且许多组件彼此非常了解 。 所以,我上面发布的所有代码实际上都可以删除。 可以删除整个框架 删除游戏对象(即其他框架中的实体)、删除组件和删除查找类型。 这些都是无用虚拟机的一部分,它违反了OOD规则,使得游戏非常慢。 无框架合成(即使用编程语言函数实现合成)如果整个合成框架被删除并且没有组件基类 ,我们如何使用合成来管理游戏对象?我们不需要用自己奇怪的语言编写虚拟机和实现游戏对象。我们可以用C++来实现它,因为这是我们游戏程序员的工作。 整个实体/组件框架已从以下提交内容中删除:https://github.com/hodgman/国防部操场/Blob/3529 f 232510 c 95 f 53112 bbfff 87 df 6BBC 6a 1 FAE/SOURce/GAME 。cpp以下是改进后的代码:https://github.com/hodgman/dod-playground/blob/ f42290 d 0217 d700 dea2e d 002 f2f 3 B1 DC 45 e8c 27c/源代码/游戏。CPP此更改包括从每个组件类型中删除“公共组件”。 构造函数被添加到每个组件类型中 OOD的主要思想是封装类的状态,但是这些类非常小而且简单,所以没有什么可隐藏的 ,它的接口只是数据描述。 然而,封装成为面向对象支柱的一个主要原因是它可以使类不变量(https://en.wikipedia.org/wiki/Class_invariant)始终为真……或者,当违反某个不变量时,您只需要检查封装的实现代码来发现错误 在这个示例代码中,我们值得添加一个构造函数来确保所有值都必须初始化的简单不变量 我更改了过于常见的“更新”方法的名称,以反映实际函数,例如 ,“移动组件”称为“更新位置”,而“避免组件”称为“解决冲突” 我删除了三段关于模板和预制组件的硬编码代码 ,即创建包含特定组件类型的游戏对象代码,并用三个C++类替换它。 更正了“虚拟无效更新”的反向模式 让游戏对象在构建过程中直接链接组件,而不是让组件通过服务定位器模式找到彼此 对象,我们不再使用以下“虚拟机”代码:for(autoi = 0;i <。kObjectCount++i) {游戏对象* go =新游戏对象(“对象”);位置组件* pos =新位置组件();pos->;x =随机浮动(界限->;xMin,界限->。xMax);pos->;y =随机浮动(界限->;yMin,界限->。Ymax);go->;添加组件(pos);精灵组件*精灵=新精灵组件();雪碧->;颜色= 1.0f雪碧->;颜色= 1.0f雪碧->;颜色= 1.0f雪碧->;sprite index = rand()% 5;雪碧->;比例尺= 1.0fgo->;添加组件(精灵);MoveComponent * move =新move ComPoint(0.5f,0.7f);go->;添加组件(移动);InPublic ComPonent * InPublic =新的InPublic ComPonent();go->;添加组件(避免);s _物体.就位_返回(go);}相反 ,它是使用普通的C++:结构化正则对象{ PositionComponentPOS精灵组成精灵;移动组件移动;避免组件避免;正则对象(const WorldBoundsComponent & amp边界) :移动(0.5f,0.7f),位置(RandomFloat(bounds.xMin,bounds.xMax),RandomFloat(bounds.yMin,bounds.yMax)),子画面(1.0f,1.0f,1.0f,rand() % 5,1.0f){ } };...regularObject . reserve(Kobjectcount);对于(自动I = 0;i <。kObjectCount++算法现在的另一个问题是算法 还记得我在开始时说过界面和算法是共生的吗,它们应该影响彼此的设计吗?“虚拟无效更新”的反向模式也不适合这种情况。 原始代码有一个主循环算法 ,其结构如下:对于(自动增长:s _ objects) {go-> Update(时间,增量时间);你可能认为这段代码非常简洁,但我认为这段代码非常糟糕。 它完全混淆了游戏中的控制流和数据流。 如果我们想了解软件、维护软件、为软件添加新功能、优化软件,甚至想让它在多个CPU内核上运行得更快,那么我们必须了解控制流和数据流 因此,“虚拟无效更新”不应出现。 相反 ,我们应该使用一个更清晰的主循环,以便更容易地演示控制流(数据流在这里仍然是混乱的,我们将在提交中稍后解决它) 用于(自动和。go : s_game->;正则对象){更新位置(deltaTime,go,s _ game->;bounds . WB);}适用于(自动和;go : s_game->;避免此操作){更新位置(增量时间 ,开始,s _ game->;bounds . WB);}适用于(自动和;go : s_game->;正则对象){解析生态列表(deltaTime,go ,s _ game->;避免这种情况);}这种样式的缺点是,对于添加的每种新类型的对象,都会在主循环中添加几行。 我将在以后的文章中解决这个问题 。 代码中仍然有一些违反OOD的地方,一些糟糕的设计选择,以及许多可以优化的地方,但是我将在以后的文章中解决这些问题。 至少目前来说,这个“修正的OOD”版本的性能并不比Aras语音中的最终ECS版本差,甚至可能超过它……我们所做的就是删除伪OOP代码,使用真正遵守OOP规则的代码(并删除100多行代码!) 我想进一步讨论下一步 ,包括解决剩余的OOD问题、不可变对象(函数式编程 ,https://en . Wikipedia . org/wiki/FunctiOnal _ programming),以及演示数据流和消息传递的好处。 在我们的面向对象代码中添加一些国防部的论证,在面向对象代码中添加一些关系技巧,删除那些“实体”类,并获得纯粹由组件组成并以不同风格相互链接的组件(指针VS事件处理),真实世界的组件容器,添加更多的优化以跟上ECS版本,以及更多的优化(如线程和SIMD) ,这些都是Aras演讲中没有提到的。 原创:https://www . game dev . net/blogs/entry/2265481-OOP-is-dead-long-live-OOP/5g进入第一年,物联网的发展越来越受欢迎!你是否有特殊技能不得而知。不要让你的物联网项目再次被忽视!继第一次人工智能优秀案例评选活动之后,2019年案例评选活动再次升级。CSDN将选择前30名优秀的物联网案例,所以快速扫描代码参与选择。丰厚的福利,等着你去拿!推荐夏季乐队的热门文章酷吗?程序员岩爆!腾讯再次加薪!员工的平均月薪是72700英镑!华为招聘了200万名人工智能医生,为什么马斯克推出了针对人工智能的脑机接口?鸿蒙OS背后的神秘人物暴露了!一笔交易只能支付0.5元才能撤销吗?这是一个痛苦的DApp …从原则到代码,很容易深入到逻辑回归模型!@程序员和“10倍工程师”都在这四个人工智能方向上追逐常见的10大Hadoop应用误解!CSDN玩得很开心!粉丝:我太认真了。我把你点的每一份“看”都当成一种享受。 go 学习笔记之值得特别关注的基础语法有哪些

(责任编辑:法治)