如何有效而正确的使用继承和多态性?

如何有效而正确的使用继承和多态性?_第1张图片

​本文是有关SystemVerilog(SV)面向对象编程( object oriented programming,OOP)的第二部分。在第一篇文章中,我们介绍了class(类)这一数据类型的基础知识和OOP的历史。在本文中,使用示例说明了如何有效而正确的使用继承和多态性,为应用通用验证方法学(Universal Verification Methodology,UVM)流程做准备。

OOP是行之有效的软件代码。它的主要特点是可以编写抽象的、高复用性的和高维护性的代码。类可用于编写可复用的验证环境、抽象数据及对其进行操作方法的建模。

继承使复用成为可能。父类现有的所有属性和方法都可以传递给新创建的类。新创建的类可以叫做子类、扩展类或是派生类。

各种OOP语言的另一个关键原则是多态。多态使同一段代码可以根据对象类型进行不同的处理。首先,我们要搞明白“继承”。

 

 

子类的属性和方法

使用子类的优势之一是:父类中的任何修改都可以传播到继承于它的所有子类中。子类包含有父类中声明的所有内容和使用者添加的其它属性和方法。继承也允许我们覆盖现有方法。这种灵活的选择和覆盖使得使用者可以采用父类中的内容和自定义的内容。这让许多良好的功能得以保留和应用。父类和子类是具有 “涟漪效应” 的。因为父类可以扩展出许多的子类,而这些子类也可以作为 “父类” 扩展出更多的子类

       如何有效而正确的使用继承和多态性?_第2张图片       图1 子类的属性

图1中,类 “Packet”只有一个简单的示意,它具有三个属性:“Command”、“Status” 和 “Data”。类 “ErrorPkt”继承了类 “Packet”,并额外添加了一个属性 “Error”。因此,类 “Packet” 是 类 “ErrorPkt” 的父类,类 “ErrorPkt” 是类 “Packet” 的子类。这样一来,类 “ErrorPkt” 就像是一个新的类一样,具有 “Command”、“Status”、“Data” 和 “Packet” 四种属性。通过这种方法,就可以使用类的所有好处。

使用者也可以像添加属性一样,将方法添加到子类中。覆盖可以将父类的方法在不被替换的情况下隐藏。如此一来,使用者就可以通过在被覆盖的方法周围添加代码来对被覆盖的方法进行少量的修改,这对于复用非常有帮助。被覆盖的方法不必存在于父类中。子类可能会有很多层,每个子类都会继承父类继承的所有内容

       如何有效而正确的使用继承和多态性?_第3张图片       图2 子类的方法

图2中的类 “Packet” 相比图1中,多出了一个方法 “SetStatus”。它的子类 “ErrorPkt”也添加了父类没有的新方法 “ShowError”,并且子类 “ErrorPkt” 中的方法 “SetStatus” 覆盖了父类 “Packet” 中的方法 “SetStatus”。SystemVerilog提供了一个 “super” 前缀,用于访问不被覆盖时就会访问的内容。“super” 会根据需要查找尽可能多的级别以找到被覆盖的方法。

所有的类都需要一个构造函数,子类也不例外子类的构造函数中的第一条语句必须调用父类的构造函数。因此,每当有一个子类继承于一个父类时,都有一条从父类到最外层子类的构造函数链。(译者注:为了更好理解这种关系,可以用家族图谱的方式进行类比。)如果没有类具有显式构造函数,SystemVerilog会插入隐式构造函数以及对基本构造函数的调用

       如何有效而正确的使用继承和多态性?_第4张图片       

图3 构造子类

如图3所示,一旦这个构造函数链中出现参数,就会出现问题。如果在构造函数中添加参数,以下两种情况都可能使编译器报错1.未定义构造函数的情况下扩展子类;2.定义了构造函数但未显式调用父类的构造函数。原因在于:通过SystemVerilog插入的对父类构造函数的隐式调用,编译器不知道作为参数传递的是什么。因此,使用者必须通过显式调用父类的构造函数来传递参数。

那么,类变量和句柄是如何在继承中工作的呢?在扩展子类时,使用者将创建一个具有父类和子类所有属性的对象的句柄,并且可以同时设置属性 “Command” 和属性 “Error”,也可以调用覆盖的方法。

将类句柄分配给类变量时,编译器可以帮助使用者从父类变量类型的角度访问属性和方法,而不管该变量具有句柄的子类对象是什么。

       如何有效而正确的使用继承和多态性?_第5张图片      

图4 调用函数 “printme”

当调用图4中的函数 “printme” 时,它希望将thing对象作为参数传递,但是使用者可以将从thing扩展的任何对象传递给函数。然而,直接从父类句柄访问子类对象中定义的任何内容是无法实现的。

 

 

上投和下投

构造子类对象并将其句柄分配给其父类的变量称为上投(up-casting)。使用者不能构造一个父类对象并将其句柄分配给子类变量。然而,如同上投的存在一样,有时需要存在一个带有子类对象句柄的父类变量。要访问这个子类对象的属性,需要将句柄下投(down-cast)为子类变量。由于不允许直接操作,SystemVerilog提供了动态函数 “$cast” 用于检查分配是否兼容。

       如何有效而正确的使用继承和多态性?_第6张图片      

图5 动态函数 “$cast”

在图4的例子中,不能通过变量 “h” 访问变量 “id”。在图5中,$cast检查t_h是否持有组件对象(或从组件扩展的任何对象)的句柄。这样一来,就可以清楚的知道它要访问的 “id” 属性。

 

 

虚拟和非虚拟的类方法

前面已经讲述了使用继承复用现有的类并扩展其行为。但是到目前为止,我们还必须了解我们正在处理扩展类以访问该行为。这正是多态发挥作用的地方。

多态是指相同代码根据其使用的对象类型不同而有不同的功能。SystemVerilog通过两种不同的方式启用多态:1.在编译时使用参数化类静态的使用;2.在运行时使用虚方法动态的使用。

现在将要展示该方法虚拟化时会发生什么。如图5所示,类“Packet”和之前一样具有方法“SetStatus()”。如果在声明的前面添加关键字 “virtual” ,那么编译器不再根据类变量的类型来决定被调用的方法,编译器会根据存储在类变量中的句柄的类型查找要调用的虚拟方法。

       如何有效而正确的使用继承和多态性?_第7张图片       图6 虚拟类方法

在图6所示的任务“run()”中,一个句柄可能被传递给类 “Packet” 或类 “ErrorPkt”。具体是哪个并不需要知道。如果在调用“p_h.SetStatus()”时将 “ErrorPkt” 对象传递给运行任务,它将调用定义在 “ErrorPkt” 中的方法 “SetStatus” 。如果在调用 “p_h.SetStatus()” 时传递了 “Packet” 的对象,它将调用 “Packet”中定义的方法。只要该对象是被忽视的或是从类 “Packet”中派生的,运行中的任务便会忽略该对象的类型及其调用的方法。

为了实现这一方法虚方法需要在所有的派生方法中具有相同的原型,即有相同的参数类型签名。如果在类 “ErrorPkt”的方法 “SetStatus” 中添加额外的参数,这种方法就不能保证正常工作。

非常重要的一点是:一旦方法被声明为虚方法,则该方法在所有子类中始终都是虚拟的。这意味着使用者无法覆盖方法的虚拟性质以使其成为非虚拟方法从进行调用的父类变量的角度来看,虚方法的原型是固定的

多态的常见用法是使用虚拟和非虚拟的方法将继承与 deep-copy() 结合在一起并构造新的对象。这就是众所周知的 clone() 。clone() 是一个虚方法,该方法将一个句柄返回到对调用对象进行深层复制得到的新对象。由于它是虚拟的,因此无需知道它是在处理父类对象还是子类对象就可以实现这一功能。

父类A中的方法 “clone()”有三个执行过程:1.构造一个A对象;2.通过调用方法 “copy” 复制自身;3.返回这个新构造对象的句柄。类A中的方法 “copy()”的执行过程是非常简单的:它只是复制对象的属性,这个对象的句柄作为RHS参数(代表等式的右侧)传递。RHS是一个指向调用方法 “clone()” 的对象的句柄。目标是通过方法 “clone()” 创建的新构造的对象。

       如何有效而正确的使用继承和多态性?_第8张图片       图7 虚拟方法clone的扩展

图7中展示的对虚方法克隆的所有扩展说明:原型始终是相同的——没有参数并返回A对象的句柄。实际上,每个clone()复用中唯一不同的是它构造的对象的类型。每个copy()的复用都会复制其本地属性,然后调用super.copy()以复制其子类的本地属性。如果copy()是非虚拟的,其参数就可以保持本地类的类型,这样就能直接访问本地属性。这正是使用者所需要的。方法“clone”的虚拟性质将有助于正确的派生方法 “copy()”,原因是一直从正确的类变量的类型调用copy()。

现在,可以尝试编写一个突发任务:该任务获取一个A对象的句柄,对其进行clone(),沿着其它任务链执行send()。这时并不需要知道它是否在处理A的对象或A的子类。父类库在多数情况下采用虚方法,这样可以使得代码具有更好的复用性。

 

 

辅助功能和抽象类

有时用户会希望限制其他人员访问类的成员,这是一种可以防止内部损坏的安全措施。向成员添加限定符 “local” 可以确保该成员只有本地类可以访问限定符 “protected” 则可以确保这个成员只能被其所在的类访问。这两个限定符都有助于向使用者隐藏一些细节,它们更常用于限制访问和允许有限的间接方式访问内部成员,这样可以防止用户依赖于实现细节。

最后需要介绍的是抽象类。抽象类通过实现核心功能成为了许多类库的基础。例如配置、报告和进程间的通信。抽象类还提供了一个API,其用于更方便的集成各种来源的基础类模型。这就是许多抽象类中存在很多 “local” 或 “protected” 的成员的原因,它们限制用户使用已公布的API。有时,API可能会要求用户提供方法的实现,例如方法 “clone” 或方法 “print”。抽象类可以声明方法的原型,但在使用时需要进行完整的实现来覆盖它。这样一来,基础类库可以调用对象的虚方法 “clone” 或虚方法 “print”,并确保已在派生的子类中实现了它。  

到此,完成了对继承和多态的介绍。在本系列有关UVM的SV OOP的第三篇文章中,将会讲述SV如何支持使用参数化类编写通用代码的模板。

 

原文出自:https://www.edn.com/inheritance-and-polymorphism-of-systemverilog-oop-for-uvm-verification/

 

系列回顾:

当工程师说"class"时,到底在说什么?

 

如何有效而正确的使用继承和多态性?_第9张图片

 

往期精彩:

30w+还送股送房?60+IC企业2019薪资全面攀升!

UVM RAL模型:用法和应用

我们准备做第二期线下培训,依旧认真且严肃

如果你突然被裁员了,你的Plan B是什么?

[彩虹糖带你入门UVM]

理解UVM-1.2到IEEE1800.2的变化,掌握这3点就够

 

如何有效而正确的使用继承和多态性?_第10张图片

你可能感兴趣的:(SV语言与UVM应用,路科验证)