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
类,该怎么办?
class SecurityService {
static void check(Device d) { if (d.id==null) throw new SecurityException() }
}
如果你想在traits中调用它,那么你就需要将它明确地投射到Device
中。这会让代码一点都不优雅,因为到处都是数据类型的转换。
2.2 @SelfType
注解
为了使该约束显式,并使类型检查器了解其自身的类型,Groovy提供了一个@SelfType
注释,该注释将:
- 让您声明实现此特性的类必须继承或实现的类型
- 如果不满足这些类型约束,则抛出编译时错误
因此,针对上面的类型约束,我们可以通过注解@groovy.transform.SelfType
来进行约束定义:
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的使用类。示例如下:
//需要添加导入,否则会找不到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
^
我们如果要进行累加操作该怎么样?解决方法是使用 =
或者-=
。正确示例如下:
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正在孵化的其他功能。