Perl入门教程 - 第5部分

编者按:这个备受推崇的系列正在进行更新。您可能对以下的新版本感兴趣,可在

Perl入门教程

本系列的第1部分
本系列的第2部分
本系列的第3部分
本系列的第4部分
本系列的第6部分

什么是对象?
我们的目标
开始
我们的对象做什么?
我们的目标,第2部分
封装
尝试!

到目前为止,我们主要自己编写程序的所有内容。Perl的一个主要优点是您不需要这样做。全世界有超过1,000人贡献了超过5,000个公用包,或称为模块,用于常见任务。

在本期中,我们将通过构建一个模块来了解模块的工作方式,在这个过程中,我们还将了解一些Perl中的面向对象编程

什么是对象?

回想一下本系列的最初文章,当时我们讨论了Perl中的两种基本数据类型:字符串和数字。还有第三个基本数据类型:对象

对象是一种方便的方法,可以将信息与您实际使用该信息时所做的事情打包在一起。对象包含的信息称为其属性,您可以使用该信息做的事情称为方法

例如,您可能有一个用于地址簿程序的AddressEntry对象 - 此对象将包含存储个人姓名、邮寄地址、电话号码和电子邮件地址的属性;以及打印美观的邮寄标签或允许您更改个人电话号码的方法

在本篇文章的过程中,我们将构建一个小的、但有用的类:配置文件信息的容器。

我们的目标

到目前为止,我们将设置各种选项的代码直接放在程序的源代码中。这不是一个好的方法。您可能想安装一个程序并允许多个用户运行它,每个用户都有自己的首选项,或者您可能想存储常用的选项集以供以后使用。您需要的是存储这些选项的配置文件。

我们将使用一种简单的纯文本格式,其中名称和值对按节分组,节由方括号中的标题名称指示。当我们想要引用配置文件中特定键的值时,我们称该键为section.name。例如,在这个简单文件中,author.firstname的值是``Doug:”

   [author]
   firstname=Doug
   lastname=Sheppard

   [site]
   name=Perl.com
   url=https://perldotcom.perl5.cn/

(如果您在版本带有数字而不是年份的古老时代使用过Windows,您会认识到这与INI文件格式相似。)

现在我们知道了我们的模块的真实世界用途,我们需要考虑它将有什么属性方法TutorialConfig对象存储什么,我们可以用它们做什么?

第一部分很简单:我们希望对象的属性与配置文件中的值相对应。

第二部分稍微复杂一些。让我们先做两件我们必须做的事情:读取一个配置文件,并从中获取值。我们将这两个方法命名为readget。最后,我们再添加一个方法,它将允许我们在程序内部设置或更改值,我们将其命名为set。这三个方法几乎涵盖了我们要做的所有事情。

开始

我们将使用TutorialConfig作为配置文件类的名称。(类的名称通常使用这种大写风格。)由于Perl通过文件名查找模块,这意味着我们将把我们的模块文件命名为TutorialConfig.pm

将以下内容放入名为TutorialConfig.pm的文件中

    package TutorialConfig;

    warn "TutorialConfig is successfully loaded!\n";
    1;

(我将在代码中穿插调试语句。在实际操作中,您可以将其删除。关键字warn对于警告很有用——您想引起用户注意,但又不想像die那样结束程序的情况。)

package关键字告诉Perl您正在定义的类的名称。这通常与模块名相同。(它不必相同,但这是一个好主意!)1;将向Perl返回一个真值,表示模块已成功加载。

现在您有一个简单的TutorialConfig模块,您可以使用use关键字在代码中使用它。将以下内容放入一个非常简单的单行程序中

    use TutorialConfig;

当运行此程序时,我们看到以下内容

    TutorialConfig is successfully loaded!

我们的对象能做什么?

在我们可以创建一个对象之前,我们需要知道如何创建它。这意味着我们必须编写一个名为new的方法,该方法将设置对象并将其返回给我们。这也是您放置任何特殊初始化代码的地方,这些代码可能在创建每个对象时运行。

我们的TutorialConfig类的new方法如下,并将它放在TutorialConfig.pm文件中的包声明之后

    sub new {
        my ($class_name) = @_;

        my ($self) = {};
        warn "We just created our new variable...\n ";

        bless ($self, $class_name);
        warn "and now it's a $class_name object!\n";

        $self->{'_created'} = 1;
        return $self;
    }

(在实际操作中,您不需要那些warn语句。)

让我们逐行分析。

首先,请注意我们使用sub来定义方法。(所有方法实际上都是特殊类型的子程序。)当我们调用new时,我们传递给它一个参数:我们想要创建的对象的类型。我们将其存储在一个名为$class_name的私有变量中。(如果您想的话,也可以向new传递额外的参数。一些模块使用它来执行特殊的初始化例程。)

接下来,我们告诉Perl,$self是一个散列。语法my ($self) = {};是一个主要用于Perl对象编程的特殊习语,我们将在一些方法中看到它是如何工作的。(如果您想了解更多,$self是一个匿名散列。)

第三,我们使用bless函数。您给这个函数两个参数:您想要将其变成对象的变量,以及您想要它成为的对象类型。这是发生魔法的地方!

第四,我们将一个名为``_created”的属性设置。这个属性并不是特别有用,但它确实显示了访问对象内容的语法:$object_name->>{property_name}

最后,现在我们已经将$self变成了一个新的TutorialConfig对象,我们返回它。

创建TutorialConfig对象的程序如下所示

    use TutorialConfig;
    $tut = new TutorialConfig;

(这里不需要使用括号,除非您的对象的new方法需要额外的参数。但如果你更习惯于写$tut = new TutorialConfig();,它也可以正常工作。)

运行此代码后,您将看到

    TutorialConfig is successfully loaded!
    We just created the variable ...
    and now it's a TutorialConfig object!

现在我们有了类,并且可以用它来创建对象,让我们让我们的类些事情!

我们的目标,第二部分

再次查看我们的目标。我们需要为我们的 TutorialConfig 模块编写三个方法: readgetset

第一个方法,read,明显需要我们告诉它我们想要读取哪个文件。注意,当我们为这个方法编写源代码时,我们必须给它 两个 参数。第一个参数是我们正在使用的对象,第二个是我们想要使用的文件名。我们将使用 return 来表示文件是否成功读取。

   sub read {
      my ($self, $file) = @_;
      my ($line, $section);

      open (CONFIGFILE, $file) or return 0;

      # We'll set a special property 
      # that tells what filename we just read.
      $self->{'_filename'} = $file;



      while ($line = <CONFIGFILE>) {

         # Are we entering a new section?
         if ($line =~ /^\[(.*)\]/) {
            $section = $1;
         } elsif ($line =~ /^([^=]+)=(.*)/) {
            my ($config_name, $config_val) = ($1, $2);
            if ($section) {
               $self->{"$section.$config_name"} = $config_val;
            } else {
               $self->{$config_name} = $config_val;
            }
         }
      }

      close CONFIGFILE;
      return 1;
   }

现在我们已经读取了一个配置文件,我们需要查看我们刚刚读取的值。我们将称这个方法为 get,它不必很复杂

    sub get {
        my ($self, $key) = @_;

        return $self->{$key};
    }

这两个方法就足够我们开始对我们的 TutorialConfig 对象进行实验了。取上面的模块和示例配置文件(或在此处下载配置文件模块),将其放入一个名为 tutc.txt 的文件中,然后运行此简单程序

    use TutorialConfig;

    $tut = new TutorialConfig;
    $tut->read('tutc.txt') or die "Couldn't read config file: $!";

    print "The author's first name is ", 
             $tut->get('author.firstname'), 
             ".\n";

(注意调用对象方法的语法:$object->method(参数)。)

当你运行这个程序时,你会看到类似这样的内容

    TutorialConfig has been successfully loaded!
    We just created the variable... 
    and now it's a TutorialConfig object!
    The author's first name is Doug.

我们现在有一个对象,可以读取配置文件并显示这些文件中的值。这已经足够好了,但我们决定通过编写一个 set 方法来使其更好,这个方法允许我们在程序内部添加或更改配置值

    sub set {
        my ($self, $key, $value) = @_;

        $self->{$key} = $value;
    }

现在让我们来测试一下

    use TutorialConfig;
    $tut = new TutorialConfig;

    $tut->read('tutc.txt') or die "Can't read config file: $!";
    $tut->set('author.country', 'Canada');

    print $tut->get('author.firstname'), " lives in ",
          $tut->get('author.country'), ".\n";

这三个方法(readgetset)是我们 TutorialConfig.pm 模块所需要的全部。更复杂的模块可能有几十个方法!

封装

你可能想知道为什么我们会有 getset 方法。为什么我们要使用 $tut->set('author.country', 'Canada') 而不是使用 $tut->{'author.country'} = 'Canada' 呢?使用方法而不是直接与对象的属性交互有两个原因。

首先,你可以一般地相信一个模块不会更改它的方法,不管它们的实现如何变化。有一天,我们可能想从使用文本文件来存储配置信息切换到使用数据库,如 MySQL 或 Postgres。我们的新 TutorialConfig.pm 模块可能会有这样的 newreadgetset 方法

      sub new {
          my ($class) = @_;
          my ($self) = {};
          bless $self, $class;
          return $self;
      }

      sub read {
          my ($self, $file) = @_;
          my ($db) = database_connect($file);
          if ($db) {
              $self->{_db} = $db;
              return $db;
          }
          return 0;
      }

      sub get {
          my ($self, $key) = @_;
          my ($db) = $self->{_db};

          my ($value) = database_lookup($db, $key);
          return $value;
      }

      sub set {
          my ($self, $key, $value) = @_;
          my ($db) = $self->{_db};

          my ($status) = database_set($db, $key, $value);
          return $status;
      }

(我们的模块将在其他地方定义 database_connectdatabase_lookupdatabase_set 例程。)

尽管整个模块的源代码已经改变,所有的方法仍然具有相同的名称和语法。使用这些方法的代码将继续正常工作,但直接操作属性的代码将会失败!

例如,假设你有一些包含此行的代码来设置配置值

     $tut->{'author.country'} = 'Canada';

这在使用原始的 TutorialConfig.pm 模块时运行正常,因为当你调用 $tut->get('author.country') 时,它会在对象的属性中查找并返回 ``Canada”,就像你预期的那样。到目前为止,一切顺利。然而,当你升级到使用数据库的新版本时,代码将不再返回正确的结果。而不是 get() 在对象属性中查找,它会去数据库,而数据库可能不包含 ``author.country”的正确值!如果你一直使用 $tut->set('author.country', 'Canada'),那么一切都会正常工作。

作为模块的作者,编写方法将允许你在不需要你的模块用户重写任何代码的情况下进行更改(错误修复、增强或甚至完全重写)。

其次,使用方法可以让你避免不可能的值。你可能有一个对象,它接受一个人的年龄作为属性。一个人的年龄必须是一个正数(你不能是 -2 岁!),因此这个对象的 age() 方法将拒绝负数。如果你绕过方法并直接操作 $obj->{'age'},你可能会在代码的其他地方造成问题(例如,一个用于计算人的出生年份的例程可能会失败或产生奇怪的结果)。

作为模块作者,您可以使用方法帮助使用您模块的程序员编写更好的软件。您可以编写一次良好的错误检查例程,它将被多次使用。

顺便说一下,某些语言通过给您提供将某些属性设置为私有的能力来强制封装。Perl 不这样做。在 Perl 中,封装不是法律,而是一个非常不错的想法。)

试试看!

  1. 我们的 TutorialConfig.pm 模块可以需要一个方法,可以将新的配置文件写入任何您想要的文件名。编写您自己的 write() 方法(使用 keys %$self 获取对象的属性键)。务必使用 or 来警告文件无法打开!

  2. 编写一个 BankAccount.pm 模块。您的 BankAccount 对象应该有 depositwithdrawbalance 方法。如果尝试提取比您拥有的更多的钱,或者存入或提取负金额,则使 withdraw 方法失败。

  3. CGI.pm 也允许您使用对象(如果愿意的话)。(每个对象代表一个 CGI 查询。)方法名称与我们上次文章中使用的 CGI 函数相同。

    use CGI;
    $cgi = new CGI;
    
    print $cgi->header(), $cgi->start_html();
    print "The 'name' parameter is ", $cgi->param('name'), ".\n";
    print $cgi->end_html();
    

尝试重写您的 CGI 程序之一,使用 CGI 对象而不是 CGI 函数。

  1. 使用 CGI 对象的一个大优点是可以将查询存储和检索到磁盘上。查看 CGI.pm 文档,了解如何使用 save() 方法存储查询,以及如何将文件句柄传递给 new 从磁盘读取它们。尝试编写一个 CGI 程序,该程序保存最近使用的查询以方便检索。

标签

反馈

这篇文章有什么问题吗?通过在 GitHub 上打开问题或拉取请求来帮助我们。