Ruby 3.0的变化

2020-12-30 07:03:44

Ruby 3.0是主要的语言版本。核心团队努力保持向后兼容性,同时提供了一些巨大而令人兴奋的新功能。

Ractors:类似线程的对象,用于实现actor模型,并最终解除GVL(全局虚拟机锁)并实现真正的并发

分离从2.7开始(已弃用),现已完全完成。这意味着关键字参数不是在哈希值之上的“语法糖”,并且它们永远不会隐式地相互转换:

代码:def old_style(name,options = {})end def new_style(name,** options)end new_style(' John',{age:10})#Ruby 2.6:有效#Ruby 2.7:警告:不建议使用最后一个参数作为关键字参数;也许应该在通话中添加**#Ruby 3.0:ArgumentError(错误的参数数量(给定2个,预期为1个))new_style(' John',年龄:10)#=>作品h = {age:10} new_style(' John',** h)#=>有效,**是必填项#仍允许不使用{}传递最后一个哈希参数:old_style(' John',年龄:10)#=>作品

注意:在Ruby站点上,有一个大而详尽的解释,解释了分离的原因,逻辑和边缘情况,该解释是在2.7年初问世的,因此在这里我们将不再赘述。

代码:block = proc {| * args,** kwargs |放置" args =#{args},kwargs =#{kwargs}" }块。 call(1,2,a:true)#Ruby 2.7:args = [1,2],kwargs = {:a => true}-如预期的那样#Ruby 2.7:args = [1,2],kwargs = {:a => true}-相同的块。 call(1,2,{a:true))#Ruby 2.7:#警告:不建议将最后一个参数用作关键字参数#args = [1,2],kwargs = {:a => true}-但尽管如此,还是提取为关键字args#Ruby 3.0:#args = [1、2,{:a => true}],kwargs = {} –不会尝试将哈希提取到关键字中,也不会出现错误/警告

原因:参数转发在2.7中引入时,只能转发全有或全无。事实证明这还不够。前导参数的重要用法之一是诸如method_missing和其他DSL定义的方法之类的情况,这些情况需要传递给嵌套方法:some_symbol +所有原始参数。

代码:def请求(method,url,标头:{})放入" #{ 方法 。大写}#{url}(headers =#{headers})" end def get(...)请求(:get,...)end get(' https://example.com',标头:{content_type:' json'}) #GET https://example.com(headers = {:content_type =>" json"})#调用和定义中都可能存在前置参数:def logging_get(message,.. )放置消息get(...)结束日志记录(' Logging',' https://example.com',标头:{content_type:' json' })

注意:“所有参数splat” ...应该是参数列表中的最后一条语句(在声明和调用上)

在方法声明中,...之前的参数只能是位置(而不是关键字)参数,并且不能具有默认值(应为SyntaxError);

在方法调用中,...之前传递的参数不能为关键字参数(应为SyntaxError);

确保彻底检查标点符号,因为任何...都是无限范围的语法,这些构造是有效的语法,但不会达到预期的效果:def委托(...)#调用时不带"() " -实际上解析为(p()...)p ...#不输出任何内容,但发出警告:警告:...在EOL时,应加括号吗? #"," 1之后意外丢失,只有一个参数:1 ... p(1 ...)#打印" 1 ..." p(1,...)#按预期方式打印:#1#5个最终代表(5)

现在可以使用语法def method()= statement定义仅一个语句的方法。 (语法不需要结尾,因此是“无尽定义”的绰号。)

原因:Ruby的终结点(与C语言类似,{}不同)对于Rubyist来说通常是可以的,但是使小型实用程序方法看起来比他们应有的繁重。对于主体仅包含一个简短语句的小型实用程序方法,例如:…“正确”的定义可能看起来如此繁重,以至于人们会决定反对该定义,以使类更具可读性(相反,使类的客户端仅执行obj.internal.empty ?本身,从而减少语义)。在这种情况下,单行快捷方式定义可能会更改对实用程序方法创建的理解:

代码:def dbg = puts(" DBG:#{呼叫者。首先}")dbg#打印:DBG:test.rb:3:< main>' #方法定义支持各种参数:def dbg_args(a,b = 1,c:,d:6,& block)= puts("传递的Args:#{[a,b,c,d ,block。call]}")dbg_args(0,c:5){7}#打印:传递了Args:[0,1,5,6,7]#对于参数定义,()是强制性的def方x = x ** 2#语法错误,意外的输入终止-因为Ruby将其视为#def square(x = x ** 2)#... eg一个具有默认值的参数,它本身是引用的,并且没有方法主体#这可以使def square(x)= x ** 2 square(100)#=> 10000#为避免混淆,禁止定义方法名称,例如#foo =,类A#SyntaxError" setter方法不能在无休止的方法定义中定义&#34 ;: def attr =(val)= @attr = val#其他后缀还可以:def attr? ()= !! @attr def attr! ()= @attr = true end#很有趣,运算符方法也可以,包括#== class A def ==(other)= true end p A。 new == 5#=> true#任何奇异的表达式都可以是方法body#这可以正常工作:def read(name)= File。读(名字)。分割(" \ n")。地图(&:strip)。拒绝(&::empty?)。 uniq。排序#或什至是什么意思?.. def weird(name)=开始数据= File。 read(名称)进程(数据)true抢救false end#方法主体内部,不带括号的方法调用会导致语法错误:def foo()= puts" bar" #^语法错误,意外的字符串文字,期望`do'或' {'或'('#这是由于解析歧义而导致的,并且与其他一些地方对齐,例如x = 1 + sin y#^语法错误,意外的tIDENTIFIER,期望keyword_do或' {&# 39;或'('

注意:最初的提议似乎是愚蠢的愚人节玩笑,然后每个人都突然喜欢上了它,并且在语法上稍作更改,它就被接受了;

该功能被标记为EXPERIMENTAL,但它不会产生警告,它是有意的,请参阅其他#17399中的讨论。

原因:这很有趣。在2.7和3.0之间讨论了两个事实:在大多数其他语言中,单行模式匹配的顺序(< pattern>< operator>< data>)与在Ruby 2.7中引入的顺序(< data> ;< pattern>)中的内容;和“向右赋值运算符”的思想=>以获得更自然的链接。然后,在某个时候,想法汇聚得最为成功。

讨论:功能#17260(主模式匹配跟踪票),功能#16670(反向顺序),功能#15921(独立的向右赋值运算符),功能#15799(废弃的“管道运算符”构想),在讨论哪种“向右赋值”出生于)

代码:#匹配并解压缩:{db:{用户:' John' ,角色:' admin' }} => {db:{user:,role:}} p [user,role]#=> [" John&#34 ;," admin"]#模式匹配作为长时间实验的向右分配:File。阅读(' test.txt')。分割(" \ n")。地图(&:strip)。拒绝(&::empty?)。首先(10)=> lines p lines#文件的前10个非空行#拆包+分配功能非常强大:(1 .. 10)。 to_a。洗牌=> [*之前,(2 .. 4)=>阈值,*在]#...之后,在输入序列中,找到2..4范围内的第一个条目,将其放入`threshold`,#并分割序列之前/之后的部分p [之前,threshold,之后]#由于随机播放,您的结果可能会有所不同:)#=> [[7,5,8],3,[1,10,6,9,4,2]]#事情真的可以很快失控:Time。现在。小时=> .. 9 | 18 .. =>非工作时间

注意:功能标记为EXPERIMENTAL,会在尝试使用时发出警告,并且将来可能会更改;

但是简单的分配用法(数据=>变量)不被认为是实验性的,并且在这里仍然存在;

一个可能不是很明显的怪癖:模式匹配只能对局部变量进行解构分配,因此使用=>作为赋值运算符,您将看到这些是语法错误:some_statement => @x some_statement => obj。 attr#的意思是调用`obj.attr =`some_statement => $ y#...虽然也许不要使用全局变量:)

在上述更改之后,重新引入了in以返回true / false(模式是否匹配),而不是引发NoMatchingPatternError。

原因:新含义允许模式匹配与控制流程中的其他构造(例如迭代和常规条件)更紧密地集成在一起。

代码:用户= {角色:' admin' ,登录:' matz' }(如果用户以{角色:' admin' ,name:}放置"授予管理范围:#{name}"结束#,否则只需进行常规操作,无需提高用户= [{名称:' John' ,角色:'用户' },{名称:' Jane' ,registered_at:时间。 new(2017,5,8)},{名称:' Barb' ,角色:' admin' },{名称:' Dave' ,角色:'用户' }] old_users_range =时间。新(2016)..时间。新增功能(2019)#仅针对某些通知选择管理员和旧用户用户。选择{|你担任{角色:' admin' } | {register_at:^ old_users_range}}#=> [{:name =>" Jane&#34 ;,:registered_at => 2017-05-08 00:00:00 +0300},{:name =>" Barb&#34 ;, :role =>" admin"}]

注意:功能标记为EXPERIMENTAL,会在尝试使用时发出警告,并且将来可能会更改。

代码:用户= [{名称:' John' ,角色:'用户' },{名称:' Jane' ,角色:&manager' },{名称:' Barb' ,角色:' admin' },{名称:' Dave' ,角色:&manager' }]#现在,您如何找到仅具有模式匹配功能的管理员?。#Ruby 3.0:[*,{name:,role:' admin' },*]#注意模式:在中间放置一些东西,放置之前和之后的物品数量未知" Admin:#{name}"结束#=>管理员:Barb#在不受限制的情况下选择值,第一个splat是非贪婪的:[* before,user,* after]的case用户将"匹配之前:#{before}"放置"匹配:#{用户}"比赛后放置&#34 ;:}"之后## end#比赛之前:[]#比赛:{:name =>" John&#34 ;,:role =>" user"}#比赛后:[{:name => " Jane&#34 ;,:role =>" manager"},{:name =>" Barb&#34 ;,:role =>" admin&# 34;},{:name =>" Dave&#34 ;,:role =>" manager"}]#选择从何处开始播放时不考虑Guard子句:[ *,用户,*](如果用户[:role] ==' admin'将"用户:#{用户}"结束#=> NoMatchingPatternError-它首先将John放入`user`中,#然后才检查了guard子句,该子句不匹配使用(模式中有多个splat),#应该恰好有两个,并且它们可能仅是第一个,而#是最后一个元素:[first_user,*,{name:,角色:'管理员' },*]#^语法错误,意外*将" Admin:#{name}" end#仍然可以在任意位置使用splat:在[{name:first_user_name},*,{name:last_user_name}]中的case用户将"第一用户:#{first_user_name},最后一个用户:#{last_user_name}& #34;结束#=>第一个用户:John,最后一个用户:Dave

注意:功能标记为EXPERIMENTAL,会在尝试使用时发出警告,并且将来可能会更改。

当类@@ variable被类的父级或所包含的模块覆盖时,将引发错误。此外,顶级类变量访问也会引发错误。

原因:具有“非直观”访问规则的类变量通常被认为是不良做法。它们对于在整个类层次结构中跟踪某些内容仍然非常有用。但是,整个层次结构共享相同的变量这一事实可能会导致难以调试的错误,因此已进行了修正,以提高似乎无意的用法。

代码:#预期用途:父类定义了所有子类都可用的变量Good @@ registry = []#假定它旨在存储所有子类def self。注册表@@ registry结束结束类GoodChild<自我防卫能力强。寄存器! @@ registry<<自我@@ registry = @@ registry。 sort#重新分配值-但它仍然是PARENT的变量结束端GoodChild。寄存器! p好。注册表#=> [GoodChild]#意外使用:该变量在子级中定义,但随后父级将其更改为Bad def self类。腐败注册表! @@ registry = []结束类BadChild<错误的@@ registry = {}#这是一些变量,它属于THIS类def self。注册表@@ registry结束结束错误。腐败注册表! #对于BadChild的作者来说可能是意外的,其祖先更改了变量BadChild。注册表#下次尝试访问变量时,将引发错误#2.7:=> []#3.0:RuntimeError(BadChild的类变量@@ registry被Bad取代)#如果所包含的模块突然更改类模块OtherRegistry @@ registry = {} end Good,则会引发相同的错误。包括OtherRegistry Good。注册表#下次尝试访问变量时,将引发错误#2.7:=> {}#3.0:RuntimeError(GoodRegistry的类变量@@ registry被OtherRegistry覆盖)

在2.7中已弃用的单例类定义中的yield现在是SyntaxError – Feature#15575

现在,分配给带编号的参数(在2.7中引入)是SyntaxError而不是警告。

关于静态或渐进类型的可能解决方案以及Ruby代码中类型声明的可能语法的讨论已经开放了多年。在3.0版本中,Ruby的核心团队决定在单独的文件和单独的工具中检查类型的类型声明。因此,从3.0开始:

类型声明的语法如下(小示例):类Dog attr_reader名称:String def initialize:(name:String)-> void def bark:(at:Person | Dog | nil)->弦尾

核心类的类型声明和标准库随语言一起提供;

rbs库随Ruby一起提供,提供了用于根据声明检查代码中实际类型的工具;用于(在某种程度上)自动检测尚未键入的代码的实际类型;

TypeProf是随Ruby 3.0捆绑提供的另一个工具(“ Type Profiler”),可通过“抽象解释”(无需实际执行代码即可遍历代码)自动检测Ruby的实际类型;

为了更深入地了解,请查看这些工具的文档;邪恶火星人的弗拉基米尔·德门蒂耶夫(Vladimir Dementiev)的这篇文章也对工具和概念进行了详细研究。

原因:主要是为了保持一致性。 Ruby 2.4中引入了Object#clone(freeze:false)作为产生未冻结对象的唯一方法。 clone(freeze:true)基本上等同于clone + Frozen。

代码:o =对象。新o。克隆(Frozen:true)。冻结? #=>在Ruby 2.7中为false。在Ruby 3.0中为true o = Object。新的。冻结o。克隆(Frozen:false)。冻结? #=>在Ruby 2.7和3.0中为false

专门的构造函数#initialize_clone(在克隆对象时调用)现在可以接收Frozen:参数(如果已将其传递给#clone)。

原因:对于复合对象,如果没有此参数,则很难解决#initialize_clone中嵌套数据的冻结/取消冻结。

代码:require'设置' set =设置[1,2,3]。冻结设置。冻结? #=>真实设置。 instance_variable_get(' @ hash')。冻结? #=>正确,如预期的那样取消冻结= set。复制(frozen:false)未冻结。冻结? #=>错误,如预期的那样冻结。 instance_variable_get(' @ hash')。冻结? #2.7:=>正确,仍然是#3.0:=>假,应该是这样-如果Set已重新定义#initialize_clone unfrozen<< 4#2.7:FrozenError(无法修改冻结的Hash:{1 => true,2 => true,3 => true})#3.0:=> #<设置:{1、2、3、4}>

注意:Ruby 3.0中适当的对象冻结引起了很多关注,这是由于引入了Ractors,如果真正地冻结了对象(因此可以安全地在并行的ractors之间共享),Ractors就会带来重要的区别。

现在,当第二个参数(绑定)传递给eval时,评估代码中的__FILE__为(eval),而__LINE__从1开始(就像没有绑定一样)。在2.7之前,它是在绑定的上下文中进行评估的(例如,绑定来自的返回文件),在2.7上会打印警告;现在该行为被认为是最终的。

原因:绑定可能会传递给eval,以便提供对评估所必需的某些上下文的访问(例如,此技术在模板引擎中经常使用);但这会使__FILE__和__LINE__指向未实际评估的代码产生了意想不到的结果,这可能会引起误解,例如在错误处理方面。

代码:#文件a.rb类A def get_binding绑定结束端#文件b.rb require_relative' a' eval(' p [__FILE__,__LINE __]')#不绑定eval(' p [__FILE__,__LINE __]',A. new。get_binding)#从另一个文件绑定# Ruby 2.6:#["(eval)",1]#[" a.rb",3]#Ruby 2.7:#["(eval)&#34 ;,1]#[" a.rb&#34 ;, 3]#警告:eval中的__FILE__可能不会返回绑定中的位置;使用Binding#source_location代替#警告:eval中的__LINE__可能不会返回绑定中的位置;使用Binding#source_location代替#Ruby 3.0:#["(eval)&#34 ;, 1]#["(eval)&#34 ;, 1]

原因:更改与Ractor的引入有关(请参见下文):当在tracer之间共享对象时,是否冻结对象产生了不同。由于范围和正则表达式都具有不变的核心数据,因此认为冻结它们是正确的做法。

代码:/ foo /。冻结? #=>正确(42 ...)。冻结? #=> true#即使使用动态插值/构造正则表达式也会冻结。 #{rand(10)} /。冻结? #=> true#...但是当使用构造函数Regexp构造它们时不是。新(' foo')。冻结? #=> false#...但范围始终冻结Range。新(' a',' b')。冻结? #=> true#定期地,由于无论如何都无法更改数据,因此冻结不会影响您的代码。 #但是,如果代码做了一些聪明的事情,则可能是这样:regexp = / ^ \ w + \ s * \ w * $ / regexp。 instance_variable_set(' @ context',:name)#2.7:好的#3.0:FrozenError(无法修改冻结的Regexp:/ ^ \ w + \ s * \ w * $ /)#...或RANGE = Time。新(2020,3,1)..时间。新(2020,9,1)def RANGE。自我。开始 。 strftime('%Y,%b%d')+' -' +自我。结束 。 strftime('%b%d')结束#2.7:OK#3.0:FrozenError(无法修改冻结的对象:2020-03-01 00:00:00 + 0200..2020-09 -01 00:00:00 +0300)#另请注意,范围冻结不是" deep&#34 ;: string_range =' a' ..' z' string_range。结束 。大写! string_range#=> ' ..' Z' #clone(freeze:false)仍然允许解冻两个:unfrozen = RANGE

......