第十三章. 角色

Roles

声明

本章翻译仅用于 Raku 学习和研究, 请支持电子版或纸质版

第十三章. 角色

角色是 mixins,可以增强你的类,就像它们的内容被定义在类里一样。一旦定义,它们的源实际上会被遗忘(与父类不同)。你可以使用角色来更改类,从现有类创建新类,以及增强单个对象。它们比继承更灵活,通常是更好的解决方案。角色用于代码重用,而类用于管理对象。

给类添加行为

构造一个空的 Butterfly 类。即使没有属性接收参数的值,你也可以为 .new 提供参数:

class Butterfly {}
my $butterfly = Butterfly.new: :common-name('Perly Cracker');

现在给你的蝴蝶命名。名字应该是 Butterfly 类的一部分吗?名字不是对象. Hamadryas guatemalena 是蝴蝶的名字。 Guatemalan Cracker,Calicó 和 Soñadoracomún 也是蝴蝶的名字。这些都是同一只蝴蝶的名字。

注意

最终,你编写的代码必须在该语言框架内运行。语法有时会让你在认知上将事物分开。

A name is not a more specific version of something the class already does and it’s not limited to butterflies or butterfly-like things. Many dissimilar things can have a common name—animals, cars, food. Not only that, but different people, cultures, or even sections of your office may choose different names. This fact does not define your thingy or its behavior. It’s not something that makes a butterfly what it is.

名字不是该类已经做过的更具体的版本,它不仅限于蝴蝶或蝴蝶般的东西。许多不同的东西可以有一个共同的名字 - 动物,汽车,食物。不仅如此,不同的人,文化,甚至办公部门都可以选择不同的名字。这个事实并没有定义你的东西或它的行为。这不是让蝴蝶成为现实的东西。

Create a role that contains everything you need for a common name. Everything about a name (and nothing else!) can show up in that role. The role doesn’t care what sort of thingy uses it, whether that’s a butterfly, a car, or a pizza. Declare it with role just as you would a class:

创建一个包含公共名字所需内容的角色。关于名字的所有内容(没有别的!)都可以显示在该角色中。这个角色并不关心什么样的东西使用它,无论是蝴蝶,汽车还是披萨。用 role 声明一个角色就像声明类一样:

role CommonName {
    has $.common-name is rw = 'An unnamed thing';
}

In fact, a role can act just like a class. You can make an object from a role. This puns the role into a class:

事实上,角色就像类一样。你可以从角色创建对象。这个角色变成了一个类:

role CommonName {
    has $.common-name is rw = 'An unnamed thing';
}

my $name = CommonName.new: :common-name('Perly Cracker');
put $name.common-name; # Perly Cracker

Apply a role to a class with does after the class name in the same way you used is for inheritance:

在类名之后使用 does 以类似于继承使用 is 的方式将角色应用于类:

class Butterfly does CommonName {};

Every Butterfly object now has a $.common-name attribute and .new now sets the :common-name using either the default or the name you provide:

每个 Butterfly 对象现在都有一个 $.common-name 属性,而 .new 现在使用默认名称或你提供的名称设置 :common-name

my $unnamed-butterfly = Butterfly.new;
put $unnamed-butterfly.common-name;   # An unnamed thing

my $butterfly = Butterfly.new: :common-name('Perly Cracker');
put $butterfly.common-name;   # Perly Cracker

You can use the same role for something completely different. An SSL certificate has a common name, although its semantic meaning is different:

对于完全不同的东西,你可以使用相同的角色。 SSL 证书具有常用名,但其语义含义不同:

class SSLCertificate does CommonName {}

Butterflies and SSL certificates are completely different things and it wouldn’t make sense for them to inherit from the same thing. However, they can use the same role.

蝴蝶和 SSL 证书是完全不同的东西,它们从同一个东西继承是没有意义的。但是,他们可以使用相同的角色。

EXERCISE 13.1Create a ScientificName role that adds an attribute to store a Str for the scientific name. Apply that role to Butterfly, create an object, and output the scientific name.

练习13.1 创建一个 ScientificName 角色,添加一个属性来存储学名的字符串。将该角色应用于 Butterfly,创建对象并输出学名。

Applying Multiple Roles

You can give a butterfly a scientific name as well as a common name by creating a different role for that. This one has several attributes:

你可以通过为蝴蝶创建不同的角色,为蝴蝶提供学名和常用名。这只蝴蝶有几个属性:

role ScientificName {
    has $.kingdom is rw;
    has $.phylum is rw;
    has $.class is rw;
    has $.order is rw;
    has $.family is rw;
    has $.genus is rw;
    has $.species is rw;
    }

You can replace the CommonName role with ScientificName and things work as before:

你可以使用 ScientificName 替换 CommonName 角色,并且事情像以前一样工作:

class Butterfly does ScientificName {};
my $butterfly = Butterfly.new: :genus('Hamadryas');
put $butterfly.genus;  # Hamadryas;

Multiple does expressions apply multiple roles:

多个 does 表达式应用多个角色:

class Butterfly does ScientificName does CommonName {};
my $butterfly = Butterfly.new:
    :genus('Hamadryas'),
    :common-name('Perly Cracker')
    ;
put $butterfly.genus;
put $butterfly.common-name;

Each role inserts its code into Butterfly so it can respond to methods from either source:

每个角色都将其代码插入 Butterfly 中,以便它可以响应来自任何一个源的方法:

Hamadryas
Perly Cracker

EXERCISE 13.2Create a role Lepidoptera to represent butterflies. Fill in everything from kingdom Animalia, phylum Athropoda, class Insecta, and order Lepidoptera. Allow the role to change the family, genus, and species. Use that role in your own Butterfly class. After you get that working add the CommonName role.

练习13.2 创建一个 Lepidoptera 代表蝴蝶的角色。填写动物界,动物门,昆虫纲和鳞翅目的一切。允许角色改变族,属和物种。在你自己的 Butterfly 类中使用该角色。完成后,添加 CommonName 角色。

Methods in Roles

You can define methods in roles too. Give the ScientificName role a .gist method to create your own human-readable text version of the object:

你也可以在角色中定义方法。为 Scientific Name 角色提供一个 .gist 方法,以创建自己的对象的人类可读文本版本:

role ScientificName {
    ...; # all the attributes specified earlier

    method gist {
        join ' > ', $.kingdom, $.genus;
        }
    }

role CommonName {
    has $.common-name is rw;
    }

class Butterfly does ScientificName does CommonName {};

my $butterfly = Butterfly.new:
    :genus('Hamadryas'),
    :common-name('Perly Cracker')
    ;
put $butterfly.genus;
put $butterfly.common-name;
put $butterfly.gist;

EXERCISE 13.3Update your Lepidoptera role to have a binomial-name method that returns a Str that combines the genus and species of the butterfly (in biospeak that’s the “binomial name”).

练习13.3 更新你的 Lepidoptera 角色以拥有 binomial-name 方法,它返回一个结合蝴蝶属和种类的字符串(在biospeak中是“二项式名称”)。

To reuse these roles you want to make them available for any code to find and load. You can store roles by themselves in files just as you can with classes. Load them with use and they are available in that scope.

要重用这些角色,你需要使它们可供任何代码查找和加载。你可以像在类中一样将文件存储在文件中。使用 use 加载它们,它们在该范围内可用。

EXERCISE 13.4Separate the Lepidoptera and CommonName roles and the Butterfly class into their own files. Load those files into your program where you create your Butterfly object. Make this program work:use Butterfly; my $butterfly = Butterfly.new: :family( 'Nymphalidae' ), :genus( 'Hamadryas' ), :species( 'perlicus' ), ; put $butterfly.binomial-name;

练习13.4 将 LepidopteraCommonName 角色还有 Butterfly类分开到它们自己的文件中。在你要创建 Butterfly 的地方加载这些文件到你的程序中。使这个程序能工作:

use Butterfly;  
my $butterfly = Butterfly.new:     
    :family(  'Nymphalidae' ),     
    :genus(   'Hamadryas' ),     
    :species( 'perlicus' ), 
;  
put $butterfly.binomial-name;

De-Conflicting Roles

If two roles try to insert the same names you may have to do extra work. Suppose that both ScientificNameand CommonName had a .gist method:

如果两个角色尝试插入相同的名称,则可能需要执行额外的工作。假设 ScientificNameCommonName 都有.gist方法:

role ScientificName {
    ...; # all the attributes specified earlier

    method gist {
        join ' > ', $.kingdom, $.genus;
        }
    }

role CommonName {
    has $.common-name is rw;

    method gist { "Common name: $.common-name" }
    }

class Butterfly does ScientificName does CommonName {};

The .gist method has an explicit signature and isn’t marked with multi. When you try to compile this you get an error telling you that two roles tried to insert the same method:

.gist 方法具有显式签名,并且未标记为 multi。当你尝试编译它时,你会收到一个错误,告诉你两个角色尝试插入相同的方法:

Method 'gist' must be resolved by class Butterfly because
it exists in multiple roles (CommonName, ScientificName)

You can add a .gist method to Butterfly. Neither role replaces a method already in the class:

你可以向 Butterfly 添加 .gist 方法。这两个角色都不替换类中已有的方法:

role ScientificName {
    ...; # all the attributes specified earlier

    method gist {
        join ' > ', $.kingdom, $.genus;
        }
    }

role CommonName {
    has $.common-name is rw;

    method gist { "Common name: $.common-name" }
    }

class Butterfly does ScientificName does CommonName {
    method gist {
        join "\n",
            join( ' > ', $.kingdom, $.genus ),
            "Common name: $.common-name";
        }
    };

Or if you want both methods from the roles you can distinguish them with different signatures (and use multi). Their role names as a type might do:

或者,如果你想要角色中的两种方法,则可以使用不同的签名区分它们(并使用 multi )。他们的角色名称作为类型可能是:

role ScientificName {
    ...; # all the attributes specified earlier

    multi method gist ( ScientificName ) {
        "$.genus $.species";
        }
    }

role CommonName {
    has $.common-name is rw;

    multi method gist ( CommonName ) {
        "Common name: $.common-name";
        }
    }

class Butterfly does ScientificName does CommonName {};

my $butterfly = Butterfly.new:
    :genus('Hamadryas' ),
    :species('perlicus'),
    :common-name( 'Perly Cracker' ),
    ;

put '1. ', $butterfly.gist( CommonName );
put '2. ', $butterfly.gist( ScientificName );

This way you get both methods:

这样你就得到了两种方法:

1. Common name: Perly Cracker
2. Hamadryas perlicus

You can have the same method in the Butterfly class as long as you declare it with multi and give it a unique signature:

你可以在 Butterfly 类中使用相同的方法,只要用 multi 声明它并给它一个唯一的签名:

class Butterfly does ScientificName does CommonName {
    multi method gist {
        join "\n", map { self.gist: $_ },
            ( ScientificName, CommonName );
        }
    };
my $butterfly = Butterfly.new:
    :genus('Hamadryas'),
    :species('perlicus'),
    :common-name('Perly Cracker')
    ;

put '1. ', $butterfly.gist( CommonName );
put '2. ', $butterfly.gist( ScientificName );
put '3. ', $butterfly.gist;

Your output shows all three and you can pick whichever you like:

你的输出显示全部三个,你可以选择你喜欢的任何一个:

1. Common name: Perly Cracker
2. Hamadryas perlicus
3. Hamadryas perlicus
Common name: Perly Cracker

Anonymous Roles

Not every role needs a name. If you want a role that you don’t expect to use again you can add it directly with but. You can apply that directly to a class name. This actually creates a new class with the role applied to it. The new class inherits from the original:

并非每个角色都需要一个名字。如果你想要一个不希望再次使用的角色,可以用 but 直接添加它。你可以将其直接应用于类名。这实际上创建了一个应用了角色的新类。新类继承自原始:

class Butterfly {};
my $class-role = Butterfly but role { has $.common-name };

put $class-role.^name; # Butterfly+{<anon|140470326869504>}
say $class-role.^mro; # ((...) (Butterfly) (Any) (Mu))

my $butterfly = $class-role.new:
    :common-name( 'Perly Cracker' );

put $butterfly.common-name;

You can do the same thing with less work by removing the variables that stored the classes:

通过删除存中储类的变量,你可以用更少的工作做同样的事情:

my $butterfly2 = ( Butterfly but role { has $.common-name } ).new:
    :common-name('Perlicus Cracker');
put $butterfly2.^name;
put $butterfly2.common-name;

That’s still messy. You can apply it to the object directly:

那仍然很混乱。你可以直接将角色应用于对象:

my $butterfly = Butterfly.new;
my $butterfly2 = $butterfly
    but role { has $.common-name is rw };
$butterfly2.common-name = 'Perlicus Cracker';
put $butterfly2.^name;
put $butterfly2.common-name;

You can even skip the variable to store the first object. Without the variable to store the initial object you get something a little shorter:

你甚至可以跳过变量来存储第一个对象。如果没有用于存储初始对象的变量,你可以获得更短的内容:

my $butterfly = Butterfly.new
    but role { has $.common-name is rw };
$butterfly.common-name = 'Perlicus Cracker';
put $butterfly.^name;
put $butterfly.common-name;

This has the drawback that the original object doesn’t know about the roles, so you can’t set the common name in the constructor. Your role has to allow the object to change the value to set a value.

这样做的缺点是原始对象不知道角色,因此你无法在构造函数中设置公共名称。你的角色必须允许对象更改值以设置值。

Adding a role to an object is handy when you have an object that you may not have created; perhaps it was an argument to your method or the return value from a method you don’t control. In this example you take an argument (and make it is copy so you can add the role). You call show-common-name once with a plain Butterfly. The subroutine sees that the object doesn’t know about common-name, so it adds it. In your second call to show-common-name your argument already has the common-name attribute so it doesn’t need show-common-name to add it:

当你有一个你可能没有创建的对象时,为一个对象添加一个角色很方便;也许这是你的方法的参数或你不能控制的方法的返回值。在这个例子中,你接受一个参数(并使它可拷贝(is copy),这样你就可以添加角色)。你用普通的 Butterfly 调用一次 show-common-name。子例程看到对象不知道 common-name,所以它添加了它。在第二次调用 show-common-name 时,你的参数已经有了 common-name 属性,所以它不需要 show-common-name 来添加它:

sub show-common-name ( $butterfly is copy ) {
    unless $butterfly.can: 'common-name' {
        put "Adding role!";
        $butterfly = $butterfly
            but role { has $.common-name is rw };
        $butterfly.common-name = 'Perlicus Cracker';
        }

    put $butterfly.common-name;
    }

# an object without the role
my $butterfly = Butterfly.new;
show-common-name( Butterfly.new );

# an object that already has the role
my $class-role = Butterfly but role { has $.common-name };
show-common-name( $class-role.new: :common-name( 'Camelia' ) );

The output shows that you added the role in your first call but not the second:

Adding role!
Perlicus Cracker
Camelia

When should you apply your role? Whenever it makes sense for your problem.

你应该在什么时候应用你的角色?每当它对你的问题有意义时。

EXERCISE 13.5Take your Lepidoptera role to its logical conclusion. Start with a new Animalia role to represent only the kingdom. Create an Arthropoda role to include the Animalia role and represent the phylum. Do this all the way down to the Hamadryas genus. From there, create a Hamadrayas class that inherits from Butterflybut does all the taxonomic roles down to the genus. From the Hamadrayas class you should be able to set a species. Make this program work:use lib <.>; use Hamadryas; my $cracker = Hamadryas.new: :species( 'perlicus' ), :common-name( 'Perly Cracker' ), ; put $cracker.binomial-name; put $cracker.common-name;

练习13.5 将你的 Lepidoptera 角色归结为合乎逻辑的结论。从新的 Animalia 角色开始,仅代表王国。创建 Arthropoda 角色以包括 Animalia 角色并代表门。一直这样做到 Hamadryas 属。从那里,创建一个继承自ButterflyHamadrayas 类,但将所有的分类角色归结为属。从 Hamadrayas 类,你应该能够设置一个物种。使这个程序工作:

use lib <.>; 
use Hamadryas;  
my $cracker = Hamadryas.new:     
    :species( 'perlicus' ),     
    :common-name( 'Perly Cracker' ),     
    ;  
put $cracker.binomial-name; put $cracker.common-name;

Summary

You can define common code in a role and reuse it with disparate things. Since it doesn’t create an inheritance relationship it’s perfectly suited for features that don’t define the basic idea of the type.

你可以在角色中定义公共代码,并将其重用于不同的东西。由于它不创建继承关系,因此非常适合未定义类型基本概念的功能。

comments powered by Disqus