美文网首页Perl小推车
第九章 管理真实的程序(七) -代码生成

第九章 管理真实的程序(七) -代码生成

作者: 可以没名字吗 | 来源:发表于2016-04-04 21:31 被阅读96次

    代码生成

    新手程序员往往会写多余的代码。一开始他们写的代码很长,再后来会学会使用函数、使用参数,再后来会使用面向对象、高阶函数和闭包--技能逐渐提升,代码越来越简练。

    当你成为一个更好的程序员时,就会写更少的代码来解决问题。使用更好的抽象,写更通用的代码,还会重用代码--甚至可以通过删除代码来添加功能,这时的你就达到了一定的境界。

    让你所写的程序来为你编程就叫元编程或代码生成。相对于代码重用,元编程能让你的抽象重用。

    AUTOLOAD技术就演示了在缺失函数或方法时的元编程:Perl的调度系统允许你自己控制在查找函数(或方法)失败时的行为。

    eval

    最简单的代码生成技术就是:构建一个包含Perl代码的字符串,并且以eval操作符来编译该字符串。不同于代码异常捕获的eval操作符,字符串的eval会在当前作用域内编译字符串的内容。

    一个常见用途就是在你无法加载一个可选的依赖时,提供一个倒退方案:

    eval { require Monkey::Tracer } or eval 'sub Monkey::Tracer::log {}';
    

    如果Monkey::Tracer不可用,其中log()函数就什么都不会做。你还得考虑关键字转义的问题。通过插入一些变量来增加复杂性:

    sub generate_accessors
    {
    my ($methname, $attrname) = @_;
    
    eval <<"END_ACCESSOR";
    sub get_$methname
    {
    my \$self = shift;
    return \$self->{$attrname};
    }
    
    sub set_$methname
    {
    my (\$self, \$value) = \@_;
    \$self->{$attrname} = \$value;
    }
    END_ACCESSOR
    }
    

    上面例子中,要是谁没注意,忘记了写反斜杠会怎么样呢?幸运的是语法高亮可能会帮助你注意到这个问题。eval每次被调用都会生成新的数据结构来表示代码,还会花费性能来编译代码。eval机制有缺点,但贵在确实简单、实用。

    带参数的闭包

    通过使用eval,构建访问器和修改器就变得简单了。而闭包允许你接受参数并且在编译时就生成代码:

    sub generate_accessors
    {
    my $attrname = shift;
    
    my $getter = sub
    {
    my $self = shift;
    return $self->{$attrname};
    };
    
    my $setter = sub
    {
    my ($self, $value) = @_;
    $self->{$attrname} = $value;
    };
    
    return $getter, $setter;
    }
    

    这段代码避免了不愉快的引用转义问题,并且每个闭包只编译一次,通过共享编译过的闭包实例还会节省内存。不同之处就是绑定的$attrname是词法变量。在长时间运行的进程中或一个类中存在大量的访问器时,这个技术非常有用。

    将访问器和修改器安装到符号表是相当容易的:

    my ($get, $set) = generate_accessors( 'pie' );
    
    no strict 'refs';
    *{ 'get_pie' } = $get;
    *{ 'set_pie' } = $set;
    

    代码作用就是将函数引用安装到了符号表,符号表就是一个名字空间,里面包含了全局可访问的符号如包全局变量、函数和方法。

    Perl内部有个叫类型团(typeglob)的数据结构,里面包含了一组名字相同但类型不同的的指针,如*spud里面包含了$spud,@spud,%spud,&spud,spud(句柄)等。通过符号表spud项就能找到*spud里的各个类型。

    所以上面那段代码解释下就是:先接收访问器和设置器;然后给类型团赋值。这样以后在调用函数get_pie时就等同于调用之前接收的那个访问器($get)。(设置器set_pie是类似的)

    赋值引用到符号表项就是安装或替换这个符号表项。存储这个函数引用到符号表,将匿名函数提升为方法。

    赋值一个符号表项为字符串,而不是一个变量名字,这就是一个符合引用。你必须禁止strict的引用检查,否则会报错。很多程序可能会这么些:

    no strict 'refs';
    *{ $methname } = sub {
    # subtle bug: strict refs disabled here too
    };
    

    但是这类代码有着相同的BUG:禁用strcit检查的范围过宽,如上例中就在函数内和函数外都禁用了strcit检查。正确的做法是仅为需要的操作禁用strcit检查:

    {
    my $sub = sub { ... };
    no strict 'refs';
    *{ $methname } = $sub;
    }
    

    如果方法名字是一个字符串而不是一个变量内容,你可以直接赋值:

    {
    no warnings 'once';
    
    (*get_pie, *set_pie) =
    generate_accessors( 'pie' );
    }
    

    直接赋值给符号表(类型团)不会违反strict检查,但是会产生告警:每个glob只使用了一次。你可以通过禁用该告警来解决这个问题。

    简化符号表的操作
    你可以使用CPAN模块Package::Stash来简化符号表的操作。

    在编译时操作

    不同于直接写出来的代码,通过eval操作生成的代码是在运行时进行编译的。当你期望一个普通函数在程序任何地方都可用时,运行时生成的函数可能达不到你的预期。(因为有可能函数还没有生成好)

    强制Perl在编译时就去运行生成代码,可以使用关键字BEGIN来包含代码块。来对比下写法上的不同:

    sub get_age { ... }
    sub set_age { ... }
    
    sub get_name { ... }
    sub set_name { ... }
    
    sub get_weight { ... }
    sub set_weight { ... }
    

    sub make_accessors { ... }
    
    BEGIN
    {
    for my $accessor (qw( age name weight ))
    {
    my ($get, $set) =make_accessors( $accessor );
    
    no strict 'refs';
    *{ 'get_' . $accessor } = $get;
    *{ 'set_' . $accessor } = $set;
    }
    }
    

    当你use一个模块时,模块中函数之外的代码都会被执行,这是因为Perl会强制将require和import放到BEGIN块中,模块内函数之外的代码都会在import()调用前执行。如果仅仅是require一个模块那是不会被放到BEGIN块中的。

    还要注意的是词法声明和词法赋值之间的相互影响,声明是在编译时发生的,而赋值在代码运行时才会发生。下面这段代码有个小错误:

    use UNIVERSAL::require;
    
    my $wanted_package = 'Monkey::Jetpack';
    
    BEGIN
    {
    $wanted_package->require;
    $wanted_package->import;
    }
    

    BEGIN块先执行,而此时$wanted_package还没被赋值,这就会抛出一个异常:尝试调用一个未定义的值。

    Class::MOP

    在Perl中可以很方便的就能实现创建函数(将函数引用安装到名字空间),但是却几乎没办法实现在动态地创建类。后来Moose和它的Class::MOP库带来了希望,它提供了一个元对象的协议---一个通过修改对象实例来控制面向对象系统的机制。

    相对于自己动手写eval或操作符号表这样弱爆了的手段,现在你拥有了更为为大的武器,不仅可以操作实例,还能操作抽象(使用了面向对象的程序的抽象)。

    创建一个类:

    use Class::MOP;
    
    my $class = Class::MOP::Class->create( 'Monkey::Wrench' );
    

    创建的同时给予属性和方法:

    my $class = Class::MOP::Class->create(
    'Monkey::Wrench' =>
    (
    attributes =>
    [
    Class::MOP::Attribute->new('$material'),
    Class::MOP::Attribute->new('$color'),
    ]
    methods =>
    {
    tighten => sub { ... },
    loosen => sub { ... },
    }
    ),
    );
    

    对于创建过的类增加属性和方法:

    $class->add_attribute(
    experience => Class::MOP::Attribute->new('$xp')
    );
    
    $class->add_method( bash_zombie => sub { ... } );
    

    MOP不仅能让你在运行时创建新实体还能让你感知现有的状态。比如,你可以使用Class::MOP::Class来侦测类的特征:

    my @attrs = $class->get_all_attributes;
    my @meths = $class->get_all_methods;
    

    类似的Class::MOP::Attribute和Class::MOP::Method也能实现创建、修改、侦测类的属性和方法。

    相关文章

      网友评论

        本文标题:第九章 管理真实的程序(七) -代码生成

        本文链接:https://www.haomeiwen.com/subject/wrbjlttx.html