在计算机编程中,Traits是面向对象编程中使用的一种概念,它表示一组可用于扩展类功能的方法
Traits提供了一组实现行为的方法,并要求来实现一组参数化所提供行为的方法。
对于对象间的通信(以及对象之间的共享),特性在面向对象的协议(接口)和mixin之间有所不同。界面可以通过方法签名来定义一个或多个行为,而特征通过完整的方法定义来定义行为:即,它包括方法的主体。相比之下,mixin包含完整的方法定义,并且还可以通过成员变量携带状态,而特征通常不会。
因此,被定义为特征的对象被创建为方法的组合,其可以被其他类使用而不需要多重继承。在命名冲突的情况下,当一个类所使用的多个特征具有同名的方法时,程序员必须明确地消除这些方法中将使用哪一种方法;从而手动解决多重继承的钻石问题。这与面向对象编程中的其他构成方法不同,在这种方法中,冲突的名称可以通过范围规则自动解决。
尽管mixin只能使用继承操作构成,但特征提供了更广泛的操作选择,包括:
对称和:合并两个不相交特征以创建新特征的操作;
重写(或不对称总和):通过向现有特征添加方法形成新特征的操作,可能会覆盖其某些方法;
别名:通过为现有方法添加新名称来创建新特征的操作;
排除:通过从现有特征中移除方法形成新特征的操作。 (将它与别名操作组合会产生浅层重命名操作)。
特质的构成方式如下:
性状组成是可交换的;添加特征的顺序并不重要。例如,给定性状S = A + B,则性状T = B + A与S相同。
冲突的方法被排除在组合物之外。
嵌套性状相当于扁平化特征;构图层次不影响特征行为。例如,给定性状S = A + X,其中X = B + C,则性状T = A + B + C与S相同 [1] 。
支持的语言编辑
Traits最初来自编程语言Self ,并受以下编程语言支持:
AmbientTalk、C++、Curl、D、ECMAScript、Groovy、Haxe、Java、JavaScript、Julia、Kotlin、Lasso OCaml、Perl、PHP、Python、Racket、Ruby、Smalltalk、Swift。
Traits技术
traits=特性
提取不同类的共性,统一处理
依靠显示模板特殊化,把代码中因不同类型发生变化的片段提取出,用统一的接口来包装
接口可以为c++类所能包含的任何东西
客户通过traits模板类公开的接口来间接访问
https://www.php.net/manual/en/language.oop5.traits.php
PHP是单继承的语言,在PHP 5.4 Traits出现之前,PHP的类无法同时从两个基类继承属性或方法。php的Traits和Go语言的组合功能有点类似,
通过在类中使用use关键字声明要组合的Trait名称,而具体某个Trait的声明使用trait关键词,Trait不能直接实例化。
<?php
trait Drive {
public $carName = ‘BMW’;
public function driving() {
echo “driving {$this->carName}\n”;
}
}
class Person {
public function age() {
echo “i am 18 years old\n”;
}
}
class Student extends Person {
use Drive;
public function study() {
echo “Learn to drive \n”;
}
}
$student = new Student();
$student->study();
$student->age();
$student->driving();
上面的例子中,Student类通过继承Person,有了age方法,通过组合Drive,有了driving方法和属性carName。
如果Trait、基类和本类中都存在某个同名的属性或者方法,最终会保留哪一个呢?通过下面的代码测试一下:
<?php
trait Drive {
public function hello() {
echo “hello 周伯通\n”;
}
public function driving() {
echo “周伯通不会开车\n”;
}
}
class Person {
public function hello() {
echo “hello 大家好\n”;
}
public function driving() {
echo “大家都会开车\n”;
}
}
class Student extends Person {
use Drive;//trait 的方法覆盖了基类Person中的方法,所以Person中的hello 和driving被覆盖
public function hello() {
echo “hello 新学员\n”;//当方法或属性同名时,当前类中的方法会覆盖 trait的 方法,所以此处hello会覆盖trait中的hello
}
}
$student = new Student();
$student->hello();
$student->driving();
hello 新学员
周伯通不会开车
因此得出结论:当方法或属性同名时,当前类中的方法会覆盖 trait的 方法,而 trait 的方法又覆盖了基类中的方法。
如果要组合多个Trait,通过逗号分隔 Trait名称:
use Trait1, Trait2;
如果多个Trait中包含同名方法或者属性时,会怎样呢?答案是当组合的多个Trait包含同名属性或者方法时,需要明确声明解决冲突,否则会产生一个致命错误。
<?php
trait Trait1 {
public function hello() {
echo “Trait1::hello\n”;
}
public function hi() {
echo “Trait1::hi\n”;
}
}
trait Trait2 {
public function hello() {
echo “Trait2::hello\n”;
}
public function hi() {
echo “Trait2::hi\n”;
}
}
class Class1 {
use Trait1, Trait2;
}
//输出:Fatal error: Trait method hello has not been applied, because there are collisions with other trait methods on Class1 in
使用insteadof和as操作符来解决冲突,insteadof是使用某个方法替代另一个,而as是给方法取一个别名,具体用法请看代码:
<?php
trait Trait1 {
public function hello() {
echo “Trait1::hello \n”;
}
public function hi() {
echo “Trait1::hi \n”;
}
}
trait Trait2 {
public function hello() {
echo “Trait2::hello\n”;
}
public function hi() {
echo “Trait2::hi\n”;
}
}
class Class1 {
use Trait1, Trait2 {
Trait2::hello insteadof Trait1;
Trait1::hi insteadof Trait2;
}
}
class Class2 {
use Trait1, Trait2 {
Trait2::hello insteadof Trait1;
Trait1::hi insteadof Trait2;
Trait2::hi as hei;
Trait1::hello as hehe;
}
}
$Obj1 = new Class1();
$Obj1->hello();
$Obj1->hi();
echo “\n”;
$Obj2 = new Class2();
$Obj2->hello();
$Obj2->hi();
$Obj2->hei();
$Obj2->hehe();
as关键词还有另外一个用途,那就是修改方法的访问控制:
<?php
trait Hello {
public function hello() {
echo “hello,我是周伯通\n”;
}
}
class Class1 {
use Hello {
hello as protected;
}
}
class Class2 {
use Hello {
Hello::hello as private hi;
}
}
$Obj1 = new Class1();
$Obj1->hello(); # 报致命错误,因为hello方法被修改成受保护的
$Obj2 = new Class2();
$Obj2->hello(); # 输出: hello,我是周伯通,因为原来的hello方法仍然是公共的
$Obj2->hi(); # 报致命错误,因为别名hi方法被修改成私有的
Trait 也能组合Trait,Trait中支持抽象方法、静态属性及静态方法,测试代码如下:
<?php
trait Hello {
public function sayHello() {
echo “Hello 我是周伯通\n”;
}
}
trait World {
use Hello;
public function sayWorld() {
echo “hello world\n”;
}
abstract public function getWorld();
public function inc() {
static $c = 0;
$c = $c + 1;
echo “$c\n”;
}
public static function doSomething() {
echo “Doing something\n”;
}
}
class HelloWorld {
use World;
public function getWorld() {
return ‘do you get World ?’;
}
}
$Obj = new HelloWorld();
$Obj->sayHello();
$Obj->sayWorld();
echo $Obj->getWorld() . “\n”;
HelloWorld::doSomething();
$Obj->inc();
$Obj->inc();
ThinkPHP 5.0开始采用trait功能(PHP5.4+)来作为一种扩展机制,可以方便的实现一个类库的多继承问题。
trait是一种为类似 PHP 的单继承语言而准备的代码复用机制。trait为了减少单继承语言的限制,使开发人员能够自由地在不同层次结构内独立的类中复用方法集。trait和类组合的语义是定义了一种方式来减少复杂性,避免传统多继承和混入类(Mixin)相关的典型问题。
但由于PHP5.4版本不支持trait的自动加载,因此如果是PHP5.4版本,必须手动导入trait类库,系统提供了一个助手函数load_trait,用于自动加载trait类库,例如,可以这样正确引入trait类库。
系统提供了一些封装好的trait类库,主要是用于控制器和模型类的扩展。这些系统内置的trait类库的根命名空间采用traits而不是trait,是为了避免和系统的关键字冲突。
trait方式引入的类库需要注意优先级,从基类继承的成员将被 trait 插入的成员所覆盖。优先顺序是来自当前类的成员覆盖了 trait的方法,而 trait则覆盖了被继承的方法。
trait类中不支持定义类的常量,在trait中定义的属性将不能在当前类中或者继承的类中重新定义。
冲突的解决
我们可以在一个类库中引入多个trait类库,如果两个trait都定义了一个同名的方法,如果没有明确解决冲突将会产生一个致命错误。
为了解决多个trait在同一个类中的命名冲突,需要使用 insteadof操作符来明确指定使用冲突方法中的哪一个。
Traits可以帮助交互作用,保证observable sequence 属性穿过界面边界,对比可以在任何环境中使用的原声Observable,Traits同时也提供contextual meaning(上下文意义), syntactical sugar and target more specific use-cases(更多的特殊应用场景) 。
由于上述原因,Traits是完全可选的,你可以在你程序的任何地方自由的使用原生的Observable sequences,因为所有的RxSwift/RxCocoa APIs都支持它们。
注意:这篇文档中的一些Traits描述(例如Driver)只是针对 RxCocoa 工程,一些只是通常 RxSwift 工程的一部分。但是同样的原理可以在其他Rx实现库中实现。
General
Why
swift 有一个强大的类型检测系统,它能够提高我们应用的正确性和稳定性,也使Rx有更直观更直接的体验。
How they work
Traits 只是一个简单包装的struct,他有一个read-only的Observable sequence属性。
struct Single
let source: Observable
}
struct Driver
let source: Observable
}
...
你可以把她们想成是建造者模式的实现。当一个Trait被建造,调用.asObservable()时,将会把它转换回一个observable sequence。
RxSwift traits
Single
Single不同于Observable(能够发送很多元素),他只能发送一个元素或者一个错误。
发射一个元素,或者一个错误。
不会产生副作用。
一个常用的场景时执行HTTP请求时,它会返回一个response或者error。Single可被用在这种模式下:你只关心一个元素,而不是无限的元素流。
创建Single
创建Single根创建Observable类似,例如:
func getRepo(_ repo: String) -> Single<[String: Any]> {
return Single<[String: Any]>.create { single in
let task = URLSession.shared.dataTask(with: URL(string: “https://api.github.com/repos/(repo)”)!) { data, _, error in
if let error = error {
single(.error(error))
return
}
guard let data = data,
let json = try? JSONSerialization.jsonObject(with: data, options: .mutableLeaves),
let result = json as? [String: Any] else {
single(.error(DataError.cantParseJSON))
return
}
single(.success(result))
}
task.resume()
return Disposables.create { task.cancel() } } } 创建完毕后,这样使用:
getRepo(“ReactiveX/RxSwift”)
.subscribe { event in
switch event {
case .success(let json):
print(“JSON: “, json)
case .error(let error):
print(“Error: “, error)
}
}
.disposed(by: disposeBag)
或者像下面这样使用subscribe(onSuccess:onError:):
getRepo(“ReactiveX/RxSwift”)
.subscribe(onSuccess: { json in
print(“JSON: “, json)
},
onError: { error in
print(“Error: “, error)
})
.disposed(by: disposeBag)
这个订阅提供了一个SingleEvent,它只能是.success(包含了一个Single的种类的元素)或者.error。这两种可能。一个事件后不会再有新的事件发射。
可以使用.asSingle()方法来改变Observable sequence成为一个Single。
Completable
Completable是一个只能complete或者error的Observable。他不能发射元素。
发射0个元素
发射一个complete或者说error事件。
没有副总用。
一种Completable的使用情景是:我们只关心运行是否结束而不关心运行的结果,可以对比Observable
创建Completable
创建Completable根创建Observable类似:
func cacheLocally() -> Completable {
return Completable.create { completable in
// Store some data locally
…
…
guard success else {
completable(.error(CacheError.failedCaching))
return Disposables.create {}
}
completable(.completed)
return Disposables.create {}
} } 创建完毕后使用
cacheLocally()
.subscribe { completable in
switch completable {
case .completed:
print(“Completed with no error”)
case .error(let error):
print(“Completed with an error: (error.localizedDescription)”)
}
}
.disposed(by: disposeBag)
或者使用subscribe(onCompleted:onError:)
cacheLocally()
.subscribe(onCompleted: {
print(“Completed with no error”)
},
onError: { error in
print(“Completed with an error: (error.localizedDescription)”)
})
.disposed(by: disposeBag)
这种订阅提供了一个CompletableEvent,它只能.completed(表示操作完成没有错误)或者.error。一个事件之后不会再发射新的事件。
Maybe
Maybe是一个介于Single和Completable之间的Observable。它能发射一个元素,不发射元素complete,或者发射一个error。
注意:这三种情况的任意一种都会中断Maybe。这就意味着completed的Maybe不能再发射元素,发射完一个元素的Maybe不能在发送Completion事件。
发射一个Completion事件,或者一个元素,或者error信息。
没有副作用
你可以在这种情况下使用Maybe:能够发射一个元素,但是并不一定必须发射一个。
创建Maybe
创建Maybe根创建Observable类似:
func generateString() -> Maybe
return Maybe
maybe(.success("RxSwift"))
// OR
maybe(.completed)
// OR
maybe(.error(error))
return Disposables.create {}
} } 创建完毕后使用
generateString()
.subscribe { maybe in
switch maybe {
case .success(let element):
print(“Completed with element (element)”)
case .completed:
print(“Completed with no element”)
case .error(let error):
print(“Completed with an error (error.localizedDescription)”)
}
}
.disposed(by: disposeBag)
或者使用subscribe(onCompleted:onError:)
generateString()
.subscribe(onSuccess: { element in
print(“Completed with element (element)”)
},
onError: { error in
print(“Completed with an error (error.localizedDescription)”)
},
onCompleted: {
print(“Completed with no element”)
})
.disposed(by: disposeBag)
你可以使用.asMaybe()将Observable转化成Maybe。
RxCocoa traits
Driver
这个是最精细的trait。它的目的是提供一种直观的方式书写UI层代码。或者其他任何情境你想要使用数据驱动应用。
不会产生错误。
观察者出现在主线程。
有副作用(shareReplayLatestWhileConnected)
为什么叫Driver
他的设计意图是为了模仿序列驱动你的应用。
例如:
通过CoreData 驱动UI
通过其他界面的元素等等(bindings)驱动UI 。
就像通常的操作系统驱动一样,一旦错误发上,你的应用就会停止回应用户输入。这些元素是在主线城被观察的这一点很重要,因为界面元素和应用逻辑通常不是线程安全的。
当然,Driver会产生副作用
例如:
实际的习惯方法
这是一个典型的初学者用法例子
let results = query.rx.text
.throttle(0.3, scheduler: MainScheduler.instance)
.flatMapLatest { query in
fetchAutoCompleteItems(query)
}
results
.map { “($0.count)” }
.bind(to: resultCount.rx.text)
.disposed(by: disposeBag)
results
.bind(to: resultsTableView.rx.items(cellIdentifier: “Cell”)) { (_, result, cell) in
cell.textLabel?.text = “(result)”
}
.disposed(by: disposeBag)
这段代码的意图是:
获取用户输入
链接服务器获取用户列表结果(每次查询)
绑定结果到两个UI元素:tableview 和用来显示数量的lable。
那么这段代码有什么问题呢?
如果fetchAutoCompleteItems observable产生错误(链接错误或者解析错误),这个错误没有绑定任何东西,UI不会回应其他新的查询。
如果fetchAutoCompleteItems 在其他后台线程种返回结果,结果会在后台线程绑定UI元素,这就可能导致non-deterministic crashes。
结果被绑定到两个界面元素。这就意味着每一次用户查询,就会做两次HTTP请求,每个界面元素做一次,这并不是我们想要的。
更合适一点的写法:
let results = query.rx.text
.throttle(0.3, scheduler: MainScheduler.instance)
.flatMapLatest { query in
fetchAutoCompleteItems(query)
.observeOn(MainScheduler.instance) // results are returned on MainScheduler
.catchErrorJustReturn([]) // in the worst case, errors are handled
}
.shareReplay(1) // HTTP requests are shared and results replayed
// to all UI elements
results
.map { “($0.count)” }
.bind(to: resultCount.rx.text)
.disposed(by: disposeBag)
results
.bind(to: resultsTableView.rx.items(cellIdentifier: “Cell”)) { (_, result, cell) in
cell.textLabel?.text = “(result)”
}
.disposed(by: disposeBag)
在一个大的系统中确保每一个需求都被正确册处理掉是很有挑战性的。但是有一个简单方式来解决这个需求,使用编译器和traits。
下面代码看起来与上面差别不大
let results = query.rx.text.asDriver() // This converts a normal sequence into a Driver
sequence.
.throttle(0.3, scheduler: MainScheduler.instance)
.flatMapLatest { query in
fetchAutoCompleteItems(query)
.asDriver(onErrorJustReturn: []) // Builder just needs info about what to return in case of error.
}
results
.map { “($0.count)” }
.drive(resultCount.rx.text) // If there is a drive
method available instead of bindTo
,
.disposed(by: disposeBag) // that means that the compiler has proven that all properties
// are satisfied.
results
.drive(resultsTableView.rx.items(cellIdentifier: “Cell”)) { (_, result, cell) in
cell.textLabel?.text = “(result)”
}
.disposed(by: disposeBag)
那到底发生了什么呢?
首先asDriver方法转变ControlProperty trait 成为Driver trait。
query.rx.text.asDriver()
注意这里没有在做其他的任何特殊的处理。Driver包含所有ControlProperty的属性。
第二个改变是
.asDriver(onErrorJustReturn: [])
任何一个observable sequence都可以被转化成Driver,只要它满足3点
不会产生错误
在主线城观察
产生副作用(shareReplayLatestWhileConnected)
那么你怎样确保满足这三点呢?只需要使用Rx操作符 . asDriver(onErrorJustReturn: [])这就等于下民的代码:
let safeSequence = xs
.observeOn(MainScheduler.instance) // observe events on main scheduler
.catchErrorJustReturn(onErrorJustReturn) // can’t error out
.shareReplayLatestWhileConnected() // side effects sharing
return Driver(raw: safeSequence) // wrap it up
最后一点是使用drive代替bindTo。
只有在Driver trait定义了drive。也就是会所如果你在代码中看到drive,那么observable sequence就不会产生错误,并且在主线程监听,这样绑定UI元素就很安全。
然而值得注意的是,理论上,我们仍然可以在ObservableType或者其他接口上定义drive方法。所以为了安全,在绑定UI元素之前使用let results: Driver<[Results]> = …创建一个短期的定义是很有必要的。但是,我们把这个问题留给读者来决定,是否这是一个确实可行的场景。
ControlProperty / ControlEvent
不会产生错误
订阅出现在主线程
监听出现在出线程
会产生其他影响。