没有符号的变量的语言如何处理动态调度/调用?

2 投票
5 回答
675 浏览
提问于 2025-04-27 22:10

动态语言允许在运行时根据变量的值来决定调用哪个方法或函数。这在 Perl 语言中有一些对比的例子:

  1. 类名

    • 常量

      Foo::Bar->some_method
      Foo::Bar::->some_method
      'Foo::Bar'->some_method
      

    这些都是相同的,除了第一个是个特殊情况。如果在当前作用域中有一个同名的子程序被定义,那么调用的结果会依赖于这个子程序的返回值,这可能会导致难以理解的错误。而引用的版本总是安全的。

    • 动态

      my $class_name = 'Foo::Bar';
      $class_name->some_method
      
  2. 方法名

    • 常量

      Some::Class->foo_bar
      
    • 动态

      my $method_name = 'foo_bar';
      Some::Class->$method_name
      
  3. 函数名

    • 常量

      foo_bar;
      (\&foo_bar)->()
      
    • 动态

      my $function_name = 'foo_bar';
      (\&$function_name)->()
      

我想知道那些变量名没有特殊符号的语言(通常是这样,或者根本没有)是怎么解决这些问题的,特别是它们的设计者是如何区分以下情况的:

  1. 解析类名 FooBar.some_method,其中类 FooBar 可能是字面量名称,也可能是一个变量,其值是类名
  2. 调用 SomeClass.foo_bar,其中方法 foo_bar 可能是字面量名称,也可能是一个变量,其值是方法名
  3. 调用 foo_bar,其中函数可能是字面量名称,也可能是一个变量,其值是函数

我主要对这个问题标签中提到的三种语言感兴趣,但如果你知道其他没有特殊符号的动态语言,也可以分享你的看法。

暂无标签

5 个回答

0

Java和其他静态语言提供了一些工具,可以通过运行时类型信息来进行反射。你需要重新引入类型信息。

class SomeClass {
     int foo(int x) {
         return x * 2;
     }

     public static void reflectionExample() 
     throws ReflectiveOperationException {
         String someMethodName = "foo";

         SomeClass obj = new SomeClass();

         // You must select the method by both method name and the signature.
         // This is because Java supports overloading.
         Method method = SomeClass.class.getMethod(
             someMethodName, Integer.TYPE
         );

         // You must cast the object back to the right return type.
         // Java will automatically 'box' and 'unbox' the values.
         int result = (Integer) method.invoke(obj, 3);

         assert result == 6;
     }
}
1

简单来说:这些其他编程语言的设计者(还有我知道的所有其他语言)并没有故意让字符串可以被当作实体名称来理解。这样的转换总是需要明确地进行。

所以,根本就没有什么需要澄清的地方。

4

在Python中,你不能把对象和包含变量名的字符串当成一样的东西来处理。如果obj是一个变量,它的值是一个对象,你可以直接调用FooBar.some_method()。但是如果你有一个字符串"FooBar",那就得另想办法。具体怎么做,取决于你认为这个变量"FooBar"在哪里(比如,它是全局变量、局部变量,还是某个属性名)。你需要在你认为的命名空间中查找这个名字,然后对找到的对象进行操作。例如,如果你想把"FooBar"当作全局变量来用,可以这样做:globals()["FooBar"].some_method()

对于函数来说,情况也是一样的,因为在Python中,函数和其他对象一样,都是对象。如果你有一个字符串"foo_bar",你认为它指的是全局命名空间中的一个函数foo_bar,你可以用globals()["foo_bar"]()来尝试调用它。

对于方法,情况基本相同,不过你总是知道你要查找的方法名在哪个命名空间:就是你想调用方法的对象的命名空间。为此,你可以使用getattr函数。如果你有一个字符串"method_name",想在FooBar上调用这个名字的方法,可以这样做:getattr(FooBar, "method_name")()

使用getattr的方法也可以用来查找其他模块命名空间中的全局名称。如果你认为function_name指的是另一个模块中的一个函数,可以这样做:getattr(other_module, function_name)

下面是一些例子:

def some_function():
    print "I am a function!"

class SomeClass(object):
    def some_method(self):
        print "I am a method!"

function_name = "some_function"
class_name = "SomeClass"
method_name = "some_method"

some_function() # call the function
globals()[function_name]() # call the function
getattr(some_module, function_name)() # if the function was in another module

SomeClass() # make an instance of the class
globals()[class_name]() # make an instance of the class
getattr(some_module, class_name)() # if the class was in another module

instance = SomeClass()
instance.some_method() # call the method
getattr(instance, method_name)() # call the method

总之,没有模糊之处,因为Python不允许你用相同的语法来处理对象和指向对象的字符串。直接尝试像"obj".method()这样的操作在Python中是明确的:"obj"是一个字符串,所以它只能表示你在尝试调用这个字符串本身的方法。Python不会试图隐式地“解码”这个字符串,看看它是否包含变量名。而且,查找类、函数或方法的操作在概念上没有区别,因为它们在Python中都是一等对象。这个过程总是一样的:首先,获取你想查找名字的命名空间;然后,进行查找。这两个步骤都必须明确。

值得注意的是,在globals()中使用这种基于字符串的查找通常被认为是比较hacky的做法。使用getattr等方法只有在处理高度动态的数据结构时(例如,从某种自描述文件中读取用户数据,文件会告诉你字段的名称)或某种元编程(例如,插件框架)时才被认为是可以接受的。对于简单的情况,像globals()["some_class"]()这样的用法会被认为是非常不符合Python风格的。

4

在Ruby中,你总是可以使用const_getsend来获取常量或调用方法:

class FooBar
    def some_method
        return 42
    end
end
class_name = 'FooBar'
puts Module.const_get(class_name).new.some_method

class SomeClass
    def foo_bar
        return 23
    end
end
method_name = 'foo_bar'
puts SomeClass.new.send(method_name)

def foo_bar
    return 123
end
function_name = 'foo_bar'
puts send(function_name)

在JavaScript中,你需要一个全局对象来查找名字:

xyz = 100; // notice: no "var" here – it's a global
var varname = 'xyz';
window[varname]; // 100

...或者你可以使用eval,但这通常会给你带来麻烦:

var x = eval;
x("var y = 10"); // INDIRECT call to eval
window.y; // 10 (it worked)
eval("var y = 11"); // direct call
window.y; // still 10, direct call in strict mode gets a new context

当你试图从一个对象中获取值时,你需要知道JavaScript不支持η转换。我们先来设置一下背景,以便更好地解释。

var object = {
  x: 10,
  say: function () {
    console.log(this.x);
  }
}

var method = 'say';
// that version (the "eta abstraction" version):
object.say(); // "this" inside of say is correctly "object"
object[method](); // equivalent

// ... isn't equivalent to this (the "eta reduction" version):
var sayit = object[method];
sayit(); // "this" inside of say is incorrectly "window" (and thus the property x won't exist)

// You can use Function#bind to work around that, but it's only available in ES5-compatible browsers (and currently pretty slow on v8/chrome)
var sayit = object[method].bind(object);
sayit(); // "this" inside of say has been forced to "object", and will print 10 correctly

// You can also, of course, close over it:
var sayit = function () { object[method]() };
sayit(); // prints 10
5

在Python和JavaScript中(我对Ruby有点生疏),你不能用字符串来表示变量名。如果你尝试这样做,系统会把它当作对字符串本身的操作:

class_name = 'Foo'
object = class_name()

> TypeError: 'str' object is not callable

首先,字符串需要通过查找上下文或作用域字典来解析:

class Foo:
   ....

object = globals()['Foo']()

或者

function Foo() ....

object = new window['Foo'];

Python提供了全局和局部字典,而JavaScript只有全局字典,所以除了普遍存在的“eval”之外,没办法通过名字获取局部变量的值。

方法也是一样:你可以使用getattr(在Python中)或者间接引用操作符[...](在JavaScript中)来查找方法:

method_name = 'foo'
method = getattr(some_object, method_name)
method()

JavaScript的代码稍微复杂一些,因为与Python不同,返回的方法指针是未绑定的:

method_name = 'foo'
method = some_object[method_name].bind(some_object)
method()

如果不使用bind,你在方法中将无法获得正确的this

var obj = {
  
  xyz: 42,
  
  method: function() {
     document.write(this.xyz)
  }

}

var method_name = 'method';

var unbound_method = obj[method_name];
unbound_method() // prints undefined

var bound_method = obj[method_name].bind(obj);
bound_method() // prints 42

更正式地说,点操作符.(相当于PHP中的::->)在Python和JavaScript中要求右边是一个标识符,而左边可以是任意表达式。由于一切都是对象,如果左边的表达式返回的是一个字符串,点操作符就会作用于这个字符串,而不是作用于一个名字恰好和这个字符串相等的变量。

(顺便提一下,技术上是可以允许点右边也有表达式的,这样foo.('bar' + 'baz')就会解析为foo.barbaz。我不知道有哪些语言支持这种语法,但看起来是getattr和类似方法的一个不错的替代方案。)

同样,调用操作符()允许左边有复杂的表达式,此外这个表达式必须解析为一个“可调用”的对象。因此,"someString"()是没有意义的,因为字符串通常是不可调用的(除非你以某种方式让它们变得可调用)。

也就是说,如果你有一个包含变量名的字符串,你必须在使用之前明确地解析它。语言本身并没有什么魔法会在后台为你处理这个问题。

撰写回答