24. Groovy 面向对象编程-Traits特性学习-第五篇 终篇

2023-02-23 17:38:57 浏览数 (1)

1. 介绍

Groovy语言学习笔记第24篇。本篇内容为traits知识点的最后一篇,将会介绍Self types(自身类型)和Limitations(限制)这两大方面的知识点。

2. 自身类型

2.1 traits的类型约束

有时我们会想写一个只能应用于某种类型的特性。例如,希望在一个类上应用一个特性,该特性扩展了另一个超出您控制范围的类,并且仍然能够调用这些方法。示例如下:

代码语言:javascript复制
class CommunicationService {
    static void sendMessage(String from, String to, String message) {       
        println "$from sent [$message] to $to"
    }
}
//创建一个类
class Device { String id }                                                  

//创建一个trait对象
trait Communicating {
    void sendMessage(Device to, String message) {
        CommunicationService.sendMessage(id, to.id, message)                
    }
}
//创建一个类,继承Device类,和Communicating 特性对象。
class MyDevice extends Device implements Communicating {}                   
def bob = new MyDevice(id:'zinyan.com')
def alice = new MyDevice(id:'Z同学')
bob.sendMessage(alice,'secret')   //输出:  zinyan.com sent [secret] to Z同学

这里很明显,Communicating特性只适用于Device。然而,没有明确的约束来表明这一点,因为traits不能扩展类。

然而,代码编译和运行非常好,因为trait方法中的id将被动态解析。

问题是,没有什么可以阻止该特性应用于任何非Device类。任何具有id的类都可以工作,而任何没有id属性的类都会导致运行时错误。

如果想启用类型检查或对trait应用@CompileStatic,问题就更复杂了:因为trait不知道自己是一个Device,所以类型检查器会抱怨说它找不到id属性。

一种可能是在trait中显式地添加getId方法,但这并不能解决所有问题。如果一个方法需要这个作为参数,并且实际上需要它是一个Device类,该怎么办?

代码语言:javascript复制
class SecurityService {
    static void check(Device d) { if (d.id==null) throw new SecurityException() }
}

如果你想在traits中调用它,那么你就需要将它明确地投射到Device中。这会让代码一点都不优雅,因为到处都是数据类型的转换。

2.2 @SelfType 注解

为了使该约束显式,并使类型检查器了解其自身的类型,Groovy提供了一个@SelfType注释,该注释将:

  • 让您声明实现此特性的类必须继承或实现的类型
  • 如果不满足这些类型约束,则抛出编译时错误

因此,针对上面的类型约束,我们可以通过注解@groovy.transform.SelfType来进行约束定义:

代码语言:javascript复制
import groovy.transform.CompileStatic
import groovy.transform.SelfType

class CommunicationService {
    static void sendMessage(String from, String to, String message) {
        println "$from sent [$message] to $to"
    }
}
class SecurityService {
    static void check(Device d) { if (d.id==null) throw new SecurityException() }
}
@SelfType(Device)
@CompileStatic
trait Communicating {
    void sendMessage(Device to, String message) {
        SecurityService.check(this)
        CommunicationService.sendMessage(id, to.id, message)
    }
}
//创建一个类
class Device { String id }
class MyDevice  implements Communicating {}

我们如果直接继承Communicating对象,就会出现以下的错误:

代码语言:javascript复制
org.codehaus.groovy.control.MultipleCompilationErrorsException: startup failed:
G:zinyan.groovy: 23: class 'MyDevice' implements trait 'Communicating' but does not extend self type class 'Device'
 @ line 23, column 28.
   class MyDevice  implements Communicating {}
                              ^

自我类型是一种强有力的方式,可以声明对特性的约束,而不必在特性中直接声明契约,也不必在任何地方使用强制转换,尽可能紧密地保持关注点的分离。

上面的错误只需要我们的类继承Device就可以解决了:

代码语言:javascript复制
class MyDevice extends Device implements Communicating {}

2.3 与Sealed注解(孵化)的差异

Groove官方正在孵化的新的注解@Sealed。它可以和@SelfType一样用于限制traits的使用类。示例如下:

代码语言:javascript复制
//需要添加导入,否则会找不到Sealed 或者 SelfType对象
import groovy.transform.Sealed
import groovy.transform.SelfType

//创建两个接口类
interface HasHeight { double getHeight() }
interface HasArea { double getArea() }

//给trait类添加上注解@SelfType和@Sealed
@SelfType([HasHeight, HasArea])     //限制trait必须是HasHeight和HasArea类才能使用                  
@Sealed(permittedSubclasses=[UnitCylinder,UnitCube])  ////限制trait必须是UnitCylinder 或 UnitCube类才能使用    
trait HasVolume {
    double getVolume() { height * area }
}
//创建一个UnitCube对象继承上面两个接口和一个trait对象。
final class UnitCube implements HasVolume, HasHeight, HasArea {
    // 定义两个数据类型为1
    double height = 1d
    double area = 1d
}

final class UnitCylinder implements HasVolume, HasHeight, HasArea {
    // for the purposes of this example: h=1, diameter=1
    // radius=diameter/2, area=PI * r^2
    double height = 1d
    double area = Math.PI * 0.5d**2  // Math.PI 是π的表达式,** 代表幂 结果值就是:π*0.5^^2
}
def z = new UnitCube().volume 
println(z) //输出:1.0
def y = new UnitCylinder().volume
println(y) //输出:0.7853981633974483

@SelfType中添加的类,代表必须全部都继承才行。

@Sealed中添加的类,代码使用其中之一都可以。

对于只需要限制单个类,两个注解没有啥区别。示例:

代码语言:javascript复制
final class Foo implements FooTrait {}

如果要限制FooTrait必须在Foo或者它的子类中使用,可以设置为:

代码语言:javascript复制
@SelfType(Foo)
trait FooTrait {}

也可以设置为:

代码语言:javascript复制
@Sealed(permittedSubclasses='Foo') 
trait FooTrait {}

PS:如果是只需要限制一个类,建议使用@SelfType。而@Sealed现在还在孵化状态,并没有正式发布,可能后续版本会有改动。不建议现在就在生产环境中使用。

3. 限制

  • 与AST(抽象语法树)转换不兼容。

特性与AST转换不兼容。如@CompileStatic将应用于trait本身(而不是应用于实现类),而其他一些将同时应用于实现类和trait。绝对不能保证AST转换会像在常规类上那样在特性上运行,所以使用它的风险自负!

  • 不允许前缀和后缀操作。

在trait中,不允许针对数据进行前缀和后缀操作运算,示例代码如下:

代码语言:javascript复制
trait Counting {
    int x
    void inc() {
        x          //禁止这样写,会出现错误:  Postfix expressions on trait fields/properties are not supported in traits.                   
    }
    void dec() {
        --x        // //禁止这样写,会出现错误:  Postfix expressions on trait fields/properties are not supported in traits.                       
    }
}
class Counter implements Counting {}
def c = new Counter()
c.inc()

上面的代码运行后会输出错误:

代码语言:javascript复制
org.codehaus.groovy.control.MultipleCompilationErrorsException: startup failed:
G:zinyan.groovy: 4: Postfix expressions on trait fields/properties are not supported in traits. @ line 4, column 10.
           x                               
            ^

G:zinyan.groovy: 7: Prefix expressions on trait fields/properties are not supported in traits. @ line 7, column 9.
           --x                             
           ^

我们如果要进行累加操作该怎么样?解决方法是使用 =或者-=。正确示例如下:

代码语言:javascript复制
trait Counting {
    int x
    void inc() {
        x =1                             
    }
    void dec() {
        x-=1                             
    }
}
class Counter implements Counting {}
def c = new Counter()
c.inc()
c.inc()
c.inc()
println(c.x) //输出3

4. 小结

到这里Groovy中关于traits的相关知识就全部介绍完毕了。

本篇相关资料可以参考Groovy官网文档:http://docs.groovy-lang.org/docs/groovy-4.0.6/html/documentation/#_self_types

下一篇将会介绍 Groovy正在孵化的其他功能。

0 人点赞