这篇文章主要讲解了“eval和alias的特性是什么”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“eval和alias的特性是什么”吧!
在一些语言中,eval
方法是将一个字符串当作表达式执行而返回一个结果的方法;在另外一些中,eval
它所传入的不一定是字符串,还有可能是抽象句法形式,Lisp 就是这种语言,并且 Lisp 也是首先提出使用 eval
方法的语言,并提出了 Evaluation 这个特性。这也使得 Lisp 这门语言可以实现脱离编译这套体系而动态执行的结果。
Lisp 中的 eval
方法预期是:将表达式作为参数传入到 eval
方法,并声明给定形式的返回值,运行时动态计算。
下面是一个 Lisp Evaluation 代码的例子( Scheme[1] 方言 RRS 及以后版本):
; 将 f1 设置为表达式 (+ 1 2 3)
(define f1 '(+ 1 2 3))
; 执行 f1 (+ 1 2 3) 这个表达式,并返回 6
(eval f1 user-initial-environment)
可能你会觉得:这只是一个简单的特性,为什么会称作黑魔法特性?
因为 Evaluation 这种可 eval 特性是很多思想、落地工具的基础。为什么这么说,下面来说几个很常见的场景。
如果你是 iOSer,你一定还会记得当年 Swift 刚刚诞生的时候,有一个主打的功能就是 REPL 交互式开发环境。
当然,作为动态性十分强大的 Lisp 和 Ruby 也有对应的 REPL 工具。例如 Ruby 的 irb 和 pry 都是十分强大的 REPL。为什么这里要提及 REPL 呢?因为在这个名字中,E 就是 eval 的意思。
REPL 对应的英文是 Read-Eval-Print Loop。
REPL 的模型让大家对于语言的学习和调试也有着增速作用,因为“Read - Eval - Print” 这种循环要比 “Code - Compile - Run - Debug” 这种循环更加敏捷。
在 Lisp 的思想中,为了实现一个 Lisp REPL ,只需要实现这三个函数和一个轮循的函数即可。当然这里我们忽略掉复杂的求值函数,因为它就是一个解释器。
有了这个思想,一个最简单的 REPL 就可以使用如下的形式表达:
# Lisp 中
(loop (print (eval (read))))
# Ruby 中
while [case]
print(eval(read))
end
大约在 2 年前,iOS 比较流行使用 JSPatch/RN 基于 JavaScriptCore 提供的 iOS 热修复和动态化方案。其核心的思路基本都是下发 JavaScript 脚本来调用 Objective-C,从而实现逻辑注入。
JSPatch 尤其被大家所知,需要编写大量的 JavaScript 代码来调用 Objective-C 方法,当然官方也看到了这一效率的洼地,并制作了 JSPatch 的语法转化器来间接优化这一过程。
但是无论如何优化,其实最大的根本问题是 Objective-C 这门语言不具备 Evaluation 的可 eval 特性,倘若拥有该特性,那其实就可以跨越使用 JavaScript 做桥接的诸多问题。
我们都知道 Objective-C 的 Runtime 利用消息转发可以动态执行任何 Objective-C 方法,这也就给了我们一个启示。假如我们自制一个轻量级解释器,动态解释 Objective-C 代码,利用 Runtime 消息转发来动态执行 Objective-C 方法,就可以实现一个“准 eval 方法”。
这种思路在 GitHub 上也已经有朋友开源出了 Demo - OCEval[2]。不同于 Clang 的编译过程,他进行了精简:
NSMethodSignature
封装方法,结合递归下降,使用 Runtime 对方法进行消息转发。利用这种思路的还有另外一个 OCRunner[3] 项目。
这些都是通过自制解释器,实现 eval 特性,进而配合 libffi 来实现。
eval
和 binding
Ruby 中的 eval 方法其实很好理解,就是将 Ruby 代码以字符串的形式作为参数传入,然后进行执行。
str = 'Hello'
puts eval("str + ' CocoaPods'") # Hello CocoaPods
上面就是一个例子,我们发现传入的代码 str + ' CocoaPods'
在 eval
方法中已经变成 Ruby 代码执行,并返回结果 'Hello CocoaPods'
字符串。
在「Podfile 的解析逻辑」中讲到, CocoaPods 中也使用了 eval
方法,从而以 Ruby 脚本的形式,执行了 Podfile
文件中的逻辑。
def self.from_ruby(path, contents = nil)
# ...
podfile = Podfile.new(path) do
begin
# 执行 Podfile 中的逻辑
eval(contents, nil, path.to_s)
rescue Exception => e
message = "Invalid `#{path.basename}` file: #{e.message}"
raise DSLError.new(message, path, e, contents)
end
end
podfile
end
当然,在 CocoaPods 中仅仅是用了 eval
方法的第一层,对于我们学习者来说肯定不能满足于此。
在 Ruby 中, Kernel
有一个方法 binding
,它会返回一个 Binding 类型的对象。这个 Binding 对象就是我们俗称的绑定,它封装了当前执行上下文的所有绑定,包括变量、方法、Block 和 self
的名称绑定等,这些绑定直接决定了面向对象语言中的执行环境。
那么这个 Binding 对象在 eval
方法中怎么使用呢?其实就是 eval
方法的第二个参数。这个在 CocoaPods 中运行 Podfile 代码中并没有使用到。我们下面来做一个例子:
def foo
name = 'Gua'
binding
end
eval('p name', foo) # Gua
在这个例子中,我们的 foo
方法就是我们上面说的执行环境,在这个环境里定义了 name
这个变量,并在方法体最后返回 binding
方法调用结果。在下面使用 eval
方法的时候,当做 Kernel#binding
入参传入,便可以成功输出 name
变量。
TOPLEVEL_BINDING
全局常量在 Ruby 中 main
对象是最顶级范围,Ruby 中的任何对象都至少需要在次作用域范围内被实例化。为了随时随地地访问 main
对象的上下文,Ruby 提供了一个名为 TOPLEVEL_BINDING
的全局常量,它指向一个封装了顶级绑定的对象。便于理解,举个例子:
@a = "Hello"
class Addition
def add
TOPLEVEL_BINDING.eval("@a += ' Gua'")
end
end
Addition.new.add
p TOPLEVEL_BINDING.receiver # main
p @a # Hello Gua
这段代码中,Binding#receiver
方法返回 Kernel#binding
消息的接收者。为此,则保存了调用执行上下文 - 在我们的示例中,是 main
对象。
然后我们在 Addition 类的实例中使用 TOPLEVEL_BINDING
全局常量访问全局的 @a
变量。
以上的简单介绍如果你曾经阅读过 SICP(Structture and Interpretation of Computer Programs)这一神书的第四章后,一定会有更加深刻的理解。
我们将所有的语句当作求值,用语言去描述过程,用与被求值的语言相同的语言写出的求值器被称作元循环;eval 在元循环中,参数是一个表达式和一个环境,这也与 Ruby 的 eval
方法完全吻合。
不得不说,Ruby 的很多思想,站在 SICP 的肩膀上。
alias
对于广大 iOSer 一定都十分了解被称作 Runtime 黑魔法的 Method Swizzling。这其实是动态语言大都具有的特性。
在 iOS 中,使用 Selector 和 Implementation(即 IMP)的指向交换,从而实现了方法的替换。这种替换是发生在运行时的。
在 Ruby 中,也有类似的方法。为了全面的了解 Ruby 中的 “Method Swizzling”,我们需要了解这几个关于元编程思想的概念:Open Class 特性与环绕别名。这两个特性也是实现 CocoaPods 插件化的核心依赖。
Open Class 特性就是在一个类已经完成定义之后,再次向其中添加方法。在 Ruby 中的实现方法就是定义同名类。
在 Ruby 中不会像 Objective-C 和 Swift 一样被认为是编译错误,后者需要使用 Category 和 Extension 特殊的关键字语法来约定是扩展。而是把同名类中的定义方法全部附加到已定义的旧类中,不重名的增加,重名的覆盖。以下为示例代码:
class Foo
def m1
puts "m1"
end
end
class Foo
def m2
puts "m2"
end
end
Foo.new.m1 # m1
Foo.new.m2 # m2
class Foo
def m1
puts "m1 new"
end
end
Foo.new.m1 # m1 new
Foo.new.m2 # m2
特异方法和 Open Class 有点类似,不过附加的方法不是附加到类中,而是附加到特定到实例中。被附加到方法仅仅在目标实例中存在,不会影响该类到其他实例。示例代码:
class Foo
def m1
puts "m1"
end
end
foo1 = Foo.new
def foo1.m2()
puts "m2"
end
foo1.m1 # m1
foo1.m2 # m2
foo2 = Foo.new
foo2.m1 # m1
# foo2.m2 undefined method `m2' for #<Foo:0x00007f88bb08e238> (NoMethodError)
其实环绕别名只是一种特殊的写法,这里使用了 Ruby 的 alias
关键字以及上文提到的 Open Class 的特性。
首先先介绍一下 Ruby 的 alias
关键字,其实很简单,就是给一个方法起一个别名。但是 alias
配合上之前的 Open Class 特性,就可以达到我们所说的 Method Swizzling 效果。
class Foo
def m1
puts "m1"
end
end
foo = Foo.new
foo.m1 # m1
class Foo
alias :origin_m1 :m1
def m1
origin_m1
puts "Hook it!"
end
end
foo.m1
# m1
# Hook it!
虽然在第一个位置已经定义了 Foo#m1
方法,但是由于 Open Class 的重写机制以及 alias
的别名设置,我们将 m1
已经修改成了新的方法,旧的 m1
方法使用 origin_m1
也可以调用到。如此也就完成了类似于 Objective-C 中的 Method Swizzling 机制。
总结一下环绕别名,其实就是给方法定义一个别名,然后重新定义这个方法,在新的方法中使用别名调用老方法。
既然说到了 alias
别名,那么就顺便说一下猴子补丁这个特性。猴子补丁区别于环绕别名的方式,它主要目的是在运行时动态替换并可以暂时性避免程序崩溃。
先聊聊背景,由于 Open Class 和环绕别名这两个特性,Ruby 在运行时改变属性已经十分容易了。但是如果我们现在有一个需求,就是 **需要动态的进行 Patch ** ,而不是只要 alias
就全局替换,这要怎么做呢?
这里我们引入 Ruby 中的另外两个关键字 refine
和 using
,通过它们我们可以动态实现 Patch。举个例子:
class Foo
def m1
puts "m1"
end
end
foo = Foo.new
foo.m1 # m1
"""
定义一个 Patch
"""
module TemproaryPatch
refine Foo do
def m1
puts "m1 bugfix"
end
end
end
using TemproaryPatch
foo2 = Foo.new
foo2.m1 # m1 bugfix
上面代码中,我们先使用了 refine
方法重新定义了 m1
方法,定义完之后它并不会立即生效,而是在我们使用 using TemporaryPatch
时,才会生效。这样也就实现了动态 Patch 的需求。
感谢各位的阅读,以上就是“eval和alias的特性是什么”的内容了,经过本文的学习后,相信大家对eval和alias的特性是什么这一问题有了更深刻的体会,具体使用情况还需要大家实践验证。这里是亿速云,小编将为大家推送更多相关知识点的文章,欢迎关注!
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。