Swift — 協議(Protocol)

Swift — 協議(Protocol)

[TOC]

協議定義了一個藍圖,規定了用來實現某一特定任務或者功能的方法、屬性,以及其他需要的東西。類、結構體和枚舉都可以遵循協議,并為協議定義的這些要求提供具體實現。某個類型能夠滿足某個協議的要求,就可以說該類型遵循這個協議。

除了遵循協議的類型必須實現的要求外,還可以對協議進行擴展,通過擴展來實現一部分要求或者實現一些附加功能,這些遵循協議的類型就能夠使用這些功能。

1. 協議的基本用法

1.1 協議語法

協議的定義方式與類、結構體和枚舉的定義非常相似

  • 基本語法
protocol SomeProtocol {
    // 這里是協議的定義部分
}
  • 如果讓自定義的類型遵循某個協議,在定義類型時,需要在類型名稱后面加上協議名稱,中間以冒號(:)隔開,如果需要遵循多個協議時,個協議之間用逗號(,)分割:
struct SomeStructure: FirstProtocol, AnotherProtocol {
    // 這里是結構體的定義部分
}
  • 如果自定義類型擁有一個父類,應該將父類名放在遵循協議名之前,以逗號分隔:
class SomeClass: SomeSuperClass, FirstProtocol, AnotherProtocol {
    // 這里是類的定義部分
}

1.2 屬性要求

我們可以在協議中添加屬性,但需要注意以下幾點:

  1. 屬性可以是實例屬性和類型屬性
  2. 屬性需要使用var修飾,不能屬于let
  3. 類型屬性只能使用static修飾,不能使用class
  4. 我們需要聲明屬性必須是可讀的或者可讀可寫的
protocol SomeProtocol {
    var propertyOne: Int { get set }
    var propertyTwo: Int { get }
    static var propertyThree: Int { get set }
}

1.3 方法要求

我們可以在協議中添加方法,但需要注意以下幾點:

  1. 可以是實例方法或類方法
  2. 像普通方法一樣放在協議定義中,但不需要大括號和方法體
  3. 協議中不支持為協議中的方法提供默認參數
  4. 協議中的類方法也只能使用static關鍵字作為前綴,不能使用class
  5. 可以使用mutating提供異變方法,以使用該方法時修改實體的屬性等。
  6. 可以定義構造方法,但是使用的時候需要使用required關鍵字
protocol SomeProtocol {
    func someMethod1()
    func someMethod2() ->Int
}

構造方法

protocol SomeProtocol {
    init(param: Int)
}

class SomeClass: SomeProtocol {
    required init(param: Int) { }
}

異變方法

protocol Togglable {
    mutating func toggle()
}

enum OnOffSwitch: Togglable {
    case off, on
    mutating func toggle() {
        switch self {
        case .off:
            self = .on
        case .on:
            self = .off
        }
    }
}

1.4 協議作為類型

盡管協議本身并未實現任何功能,但是協議可以被當做一個功能完備的類型來使用。協議作為類型使用,有時被稱作「存在類型」,這個名詞來著存在著一個類型T,該類型遵循協議T。

協議可以像其他普通類型一樣使用,使用場景如下:

  • 作為函數、方法或構造器中的參數類型或返回值類型
  • 作為常量、變量或屬性的類型
  • 作為數組、字典或其他容器中的元素類型
protocol SomeProtocol { }

class SomeClass {
    required init(param: SomeProtocol) {}
}

1.5 其他

  • 協議還可以被繼承
  • 可以在擴展里面遵循協議
  • 在擴展里面聲明采納協議
  • 使用合成來采納協議
  • 可以定義由類專屬協議,只需要繼承自AnyObject
  • 協議可以合成
  • 協議也可以擴展

更多的關于協議的用法請參考:

Protocols

以及它的譯文:

SwiftGG 協議

2. 協議中方法的調用

舉個例子,在數學中我們會求某個圖形的面積,但是不同形狀求面積的公式是不一樣的,如果用代碼來實現可以怎么來實現呢?

首先我們可以通過繼承父類的方法來實現,但是在這里我們就可以使用協議來實現:

protocol Shape {
    var area: Double {get}
}

class Circle: Shape{
    var radius: Double
   
    init(_ radius: Double) {
        self.radius = radius
    }
    
    var area: Double{
        get{
            return radius * radius * 3.14
        }
    }
}
class Rectangle: Shape{
    var width, height: Double
    init(_ width: Double, _ height: Double) {
        self.width = width
        self.height = height
    }
    
    var area: Double{
        get{
            return width * height
        }
    }
}

var circle: Shape = Circle.init(10.0)
var rectangle: Shape = Rectangle.init(10.0, 20.0)

print(circle.area)
print(rectangle.area)

<!--打印結果-->
314.0
200.0

此時的打印結果是符合我們的預期的。

我們知道協議可以擴展,此時我們把協議的代碼修改成如下:

protocol Shape {
//    var area: Double {get}
}
extension Shape{
    var area: Double {
        get{return 0.0}
    }
}

<!--打印結果-->
0.0
0.0

此時并沒有如我們預期的打印,如果我們聲明變量的時候寫成如下呢:

var circle: Circle = Circle.init(10.0)
var rectangle: Rectangle = Rectangle.init(10.0, 20.0)

<!--打印結果-->
314.0
200.0

此時的打印就符合我們的預期了。

其實我們也能夠清楚的了解到為什么會打印0.0,在Swift 方法調度這篇文章中我們介紹了extension中聲明的方法是靜態調用的,也就是說在編譯后當前代碼的地址已經確定,我們無法修改,當聲明為Shap類型后,默認調用的就是Shape extension中的屬性的get方法。下面我們在通過sil代碼來驗證一下,關于生成sil代碼的方法,請參考我以前的文章。

為了方便查看,我們精簡并修改代碼為如下:

protocol Shape {
//    var area: Double {get}
}
extension Shape{
    var area: Double {
        get{return 0.0}
    }
}

class Circle: Shape{
    var radius: Double
   
    init(_ radius: Double) {
        self.radius = radius
    }
    
    var area: Double{
        get{
            return radius * radius * 3.14
        }
    }
}

var circle: Shape = Circle.init(10.0)

var a = circle.area

生成的sil代碼:


image

通過sil代碼我們可以清晰的看到,這里直接調用的Shape.area.getter方法。

下面我們換一些簡單的代碼再次看一下:

protocol PersonProtocol {
    func eat()
}
extension PersonProtocol{
    func eat(){ print("PersonProtocol eat") }
}
class Person: PersonProtocol{
    func eat(){ print("Person eat") }
}
let p: PersonProtocol = Person()
p.eat()
let p1: Person = Person()
p1.eat()

<!--打印結果-->
Person eat
Person eat

可以看到上面這段代碼的打印結果都是Person eat,那么為什么會打印相同的結果呢?首先通過代碼我們可以知道,在PersonProtocol中聲明了eat方法。對于聲明的協議方法,如果類中也實現了,就不會調用協議擴展中的方法。上面的屬性的例子中并沒有在協議中聲明屬性,只是在協議擴展中添加了一個屬性。下面我們看看上面這段代碼的sil代碼:

image

首先我們可以看到,對于兩個eat方法的確實存在不同,首先聲明為協議類型的變量調用eat方法是通過witness_method調用,另一個則是通過class_method調用。

  • witness_method是通過PWT(協議目擊表)獲取對應的函數地址
  • class_method是通過類的函數表來查找函數進行調用
image

在剛剛sil代碼中我們可以找到sil_witness_table,在里面有PersonProtocol.eat方法,找到PersonProtocol.eat方法可以發現里面是調用class_method尋找的類中VTablePerson.eat方法。

如果我們不在協議中聲明eat方法:

protocol PersonProtocol {
//    func eat()
}
extension PersonProtocol{
    func eat(){ print("PersonProtocol eat") }
}
class Person: PersonProtocol{
    func eat(){ print("Person eat") }
}
let p: PersonProtocol = Person()
p.eat()
let p1: Person = Person()
p1.eat()

<!--打印結果-->
PersonProtocol eat
Person eat

查看sil代碼:

image

此時我們可以看到,對于不在協議中聲明方法的時候,依然是直接調用(靜態調用)。

所以對于協議中方法的調度:

  • 對于不在協議中聲明的方法
    • 在協議擴展中有實現就是直接調用
    • 在遵循協議的實體中按照其調度方式決定
    • 兩處都實現了,聲明的實例是協議類型則直接調用協議擴展中的方法,反之調用遵循協議實體中的方法
  • 對于聲明在協議中的方法
    • 如果遵循該協議的實體實現了該方法,則通過PWT協議目擊表查找到實現的方法進行調用(與聲明變量的類型無關)
    • 如果遵循協議的實體沒實現,協議擴展實現了,則會調用協議擴展中的方法

3. 協議原理探索

在上面探索協議中的方法調用的時候,我們提到過PWT也就是Protocol witness table,協議目擊表,那么它存儲在什么地方呢?我們在Swift 方法調度這篇文章中講過,V-Table是存儲在metadata中的,那么我們就探索一下PWT的存儲位置。

3.1 內存占用

首先我們先來看看如下代碼的的打印結果:

protocol Shape {
    var area: Double { get }
}
class Circle: Shape {
    var radius: Double

    init(_ radius: Double) {
        self.radius = radius
    }

    var area: Double{
        get{ return radius * radius * 3.14 }
    }
}

var circle: Shape = Circle(10.0)
print(MemoryLayout.size(ofValue: circle))
print(MemoryLayout.stride(ofValue: circle))

var circle1: Circle = Circle(10.0)
print(MemoryLayout.size(ofValue: circle1))
print(MemoryLayout.stride(ofValue: circle1))

<!--打印結果-->
40
40
8
8

3.2 lldb探索內存結構

看到這個打印結果我能第一時間想到的就是生命為協議類型會存儲更多的信息。生命為類的時候,存儲的是類的實例對象的指針8字節。下面我們通過lldb調試來探索一下這個40字節都存儲了什么信息。

image

3.3 sil 探索內存結構

通過lldb我們可以看到其內部應該存儲著一些信息,那么具體存了什么呢?我們在看看sil代碼:

image

在sil代碼中我們可以看到,在初始化circle這個變量的時候使用到了init_existential_addr,查看SIL文檔

image

譯文:用一個準備好包含類型為$T的存在容器部分初始化%0引用的內存。該指令的結果是一個地址,該地址引用了所包含值的存儲空間,該存儲空間仍然沒有初始化。包含的值必須存儲為-d或copy_addr-ed,以便完全初始化存在值。如果存在容器的值未初始化時需要銷毀,則必須使用deinit_existential_addr來完成此操作。可以像往常一樣使用destroy_addr銷毀完全初始化的存在性容器。銷毀一個部分初始化存在容器的addr是未定義的行為。

文檔中的意思是,使用了包含$Texistential container來初始化%0引用的內存。在這里就是使用包含Circleexistential container來初始化circle引用的內存,簡單來說就是將circle包裝到了一個existential container初始化的內存。

existential container是編譯器生成的一種特殊的數據類型,也用于管理遵守了相同協議的協議類型。因為這些塑化劑類型的內存空間尺寸不同,使用existential container進行管理可以實現存儲一致性。

3.4 IR代碼探索內存結構

那么這個existential container都包裝了什么呢?目前通過sil代碼是看不出來什么了,那么我們就看看IR代碼:

; 一個結構體,占用24字節內存的數組,wift.type指針, i8*指針
%T4main5ShapeP = type { [24 x i8], %swift.type*, i8** }

define i32 @main(i32 %0, i8** %1) #0 {
entry:
  %2 = bitcast i8** %1 to i8*
  ; main.Circle 的 metadata
  %3 = call swiftcc %swift.metadata_response @"type metadata accessor for main.Circle"(i64 0) #7
  %4 = extractvalue %swift.metadata_response %3, 0
  ;init放
  %5 = call swiftcc %T4main6CircleC* @"main.Circle.__allocating_init(Swift.Double) -> main.Circle"(double 1.000000e+01, %swift.type* swiftself %4)
  ; 存%4 也就是metadata,存到T4main5ShapeP結構體中,這里存的位置是第二個位置
  store %swift.type* %4, %swift.type** getelementptr inbounds (%T4main5ShapeP, %T4main5ShapeP* @"main.circle : main.Shape", i32 0, i32 1), align 8
  ; 存pwt 也就是協議目擊表,存到第三個位置
  store i8** getelementptr inbounds ([2 x i8*], [2 x i8*]* @"protocol witness table for main.Circle : main.Shape in main", i32 0, i32 0), i8*** getelementptr inbounds (%T4main5ShapeP, %T4main5ShapeP* @"main.circle : main.Shape", i32 0, i32 2), align 8
  ; 存放%5到二級指針,%5是init出來的對象,所以這里也就是個HeapObject結構,也就是T4main6CircleC結構體的第一個8字節內存空間處
  store %T4main6CircleC* %5, %T4main6CircleC** bitcast (%T4main5ShapeP* @"main.circle : main.Shape" to %T4main6CircleC**), align 8
}

從IR代碼中我們可以知道,這里面的存儲是一個結構體,結構體中主要分為三個方面:

  1. 一個連續的24字節空間
  2. 一個存放metadata的指針
  3. 存放pwt指針

3.5 仿寫

下面我們就來仿寫一下這個結構:

struct HeapObject {
    var type: UnsafeRawPointer
    var refCount1: UInt32
    var refCount2: UInt32
}

struct protocolData {
    //24 * i8 :因為是8字節讀取,所以寫成3個指針
    var value1: UnsafeRawPointer
    var value2: UnsafeRawPointer
    var value3: UnsafeRawPointer
    //type 存放metadata,目的是為了找到Value Witness Table 值目錄表
    var type: UnsafeRawPointer
    // i8* 存放pwt指針
    var pwt: UnsafeRawPointer
}

3.5.1 類遵循協議重綁定

進行內存的重新綁定:

protocol Shape {
    var area: Double { get }
}
class Circle: Shape {
    var radius: Double

    init(_ radius: Double) {
        self.radius = radius
    }

    var area: Double{
        get{ return radius * radius * 3.14 }
    }
}

var circle: Shape = Circle(10.0)

// 將circle強轉為protocolData結構體
withUnsafePointer(to: &circle) { ptr in
    ptr.withMemoryRebound(to: protocolData.self, capacity: 1) { pointer in
        print(pointer.pointee)
    }
}

<!--打印結果-->
protocolData(value1: 0x00000001006082b0, value2: 0x0000000000000000, value3: 0x0000000000000000, type: 0x0000000100008180, pwt: 0x0000000100004028)
image

通過lldb查看:

  1. 我們也可以看到對應HeapObject結構
    1. 該結構存儲的是Circle的實例變量
    2. 并且在這里面的metadataprotocolData里面的存儲的metadata的地址是一致的;
  2. 通過cat address命令查看pwt對應的指針,可以看到這段內存對應的就是SwiftProtocol.Circleprotocol witness table

至此我們就清楚的找到你了PWT的存儲位置,PWT存在協議類型實例的內存結構中。

3.5.2 結構體遵循協議重綁定

在上面這個例子中我們使用的是類,我們知道類是引用類型,如果換成結構體呢?

protocol Shape {
    var area: Double {get}
}
struct Rectangle: Shape{
    var width, height: Double
    init(_ width: Double, _ height: Double) {
        self.width = width
        self.height = height
    }

    var area: Double{
        get{
            return width * height
        }
    }
}

var rectangle: Shape = Rectangle(10.0, 20.0)


struct HeapObject {
    var type: UnsafeRawPointer
    var refCount1: UInt32
    var refCount2: UInt32
}


struct protocolData {
    //24 * i8 :因為是8字節讀取,所以寫成3個指針
    var value1: UnsafeRawPointer
    var value2: UnsafeRawPointer
    var value3: UnsafeRawPointer
    //type 存放metadata,目的是為了找到Value Witness Table 值目錄表
    var type: UnsafeRawPointer
    // i8* 存放pwt指針
    var pwt: UnsafeRawPointer
}

// 將circle強轉為protocolData結構體
withUnsafePointer(to: &rectangle) { ptr in
    ptr.withMemoryRebound(to: protocolData.self, capacity: 1) { pointer in
        print(pointer.pointee)
    }
}

<!--打印結果-->
protocolData(value1: 0x4024000000000000, value2: 0x4034000000000000, value3: 0x0000000000000000, type: 0x0000000100004098, pwt: 0x0000000100004028)
image

此時我們可以看到,此時并沒有存儲一個HeapObject結構的指針,而是直接存儲Double類型的值,metadatapwt沒有變。

在看下IR代碼:

define i32 @main(i32 %0, i8** %1) #0 {
entry:
  %2 = bitcast i8** %1 to i8*
  %3 = call swiftcc { double, double } @"main.Rectangle.init(Swift.Double, Swift.Double) -> main.Rectangle"(double 1.000000e+01, double 2.000000e+01)
  ; 10
  %4 = extractvalue { double, double } %3, 0
  ; 20
  %5 = extractvalue { double, double } %3, 1
  ;metadata
  store %swift.type* bitcast (i64* getelementptr inbounds (<{ i8**, i64, <{ i32, i32, i32, i32, i32, i32, i32 }>*, i32, i32 }>, <{ i8**, i64, <{ i32, i32, i32, i32, i32, i32, i32 }>*, i32, i32 }>* @"full type metadata for main.Rectangle", i32 0, i32 1) to %swift.type*), %swift.type** getelementptr inbounds (%T4main5ShapeP, %T4main5ShapeP* @"main.rectangle : main.Shape", i32 0, i32 1), align 8
  ;pwt
  store i8** getelementptr inbounds ([2 x i8*], [2 x i8*]* @"protocol witness table for main.Rectangle : main.Shape in main", i32 0, i32 0), i8*** getelementptr inbounds (%T4main5ShapeP, %T4main5ShapeP* @"main.rectangle : main.Shape", i32 0, i32 2), align 8
  ;存%4 也就是10
  store double %4, double* getelementptr inbounds (%T4main9RectangleV, %T4main9RectangleV* bitcast (%T4main5ShapeP* @"main.rectangle : main.Shape" to %T4main9RectangleV*), i32 0, i32 0, i32 0), align 8
  ; 存%5 也就是20
  store double %5, double* getelementptr inbounds (%T4main9RectangleV, %T4main9RectangleV* bitcast (%T4main5ShapeP* @"main.rectangle : main.Shape" to %T4main9RectangleV*), i32 0, i32 1, i32 0), align 8
}

通過IR代碼我們可以看到:

  • 對于metadatapwt的存儲依舊
  • 然后存儲了兩個Double值,并沒有存儲HeapObject類型的指針

那么如果有3個屬性呢?

struct Rectangle: Shape{
    var width, width1, height: Double
    init(_ width: Double, _ width1: Double, _ height: Double) {
        self.width = width
        self.width1 = width1
        self.height = height
    }

    var area: Double{
        get{
            return width * height
        }
    }
}

<!--內存綁定后的打印結果-->
protocolData(value1: 0x4024000000000000, value2: 0x4034000000000000, value3: 0x403e000000000000, type: 0x0000000100004098, pwt: 0x0000000100004028)

這個三個Value的值分別是10,20,30

那如果是4個呢?

struct Rectangle: Shape{
    var width, width1, height, height1: Double
    init(_ width: Double, _ width1: Double, _ height: Double, _ height1: Double) {
        self.width = width
        self.width1 = width1
        self.height = height
        self.height1 = height1
    }

    var area: Double{
        get{
            return width * height
        }
    }
}

var rectangle: Shape = Rectangle(10.0, 20.0, 30.0, 40.0)

<!--內存綁定后的打印結果-->
protocolData(value1: 0x0000000100715870, value2: 0x0000000000000000, value3: 0x0000000000000000, type: 0x00000001000040c0, pwt: 0x0000000100004050)

此時并沒有直接看到Double值了,查看value1的內存:

image

此時我們可以看到,這個內存中存儲了10,20,30,40這四個值。

所以如果我們需要存儲的數據超過了24 x i8*,也就是24字節時,就會開辟內存空間進行存儲。這里只存儲指向新開辟內存空間的指針。

這里的順序是,如果不夠存儲就直接開辟內存空間,存儲值,記錄指針。而不是先存儲不夠了在開辟內存空間。

我們都知道,結構體是值類型,如果超過這24字節的存儲空間就會開辟內存用來存儲結構體中的值,如果此時發生拷貝會是神馬結構呢?下面我們就來驗證一下:

結構體拷貝:

protocol Shape {
    var area: Double {get}
}
struct Rectangle: Shape{
    var width, width1, height, height1: Double
    init(_ width: Double, _ width1: Double, _ height: Double, _ height1: Double) {
        self.width = width
        self.width1 = width1
        self.height = height
        self.height1 = height1
    }

    var area: Double{
        get{
            return width * height
        }
    }
}

var rectangle: Shape = Rectangle(10.0, 20.0, 30.0, 40.0)
var rectangle1 = rectangle

struct HeapObject {
    var type: UnsafeRawPointer
    var refCount1: UInt32
    var refCount2: UInt32
}


struct protocolData {
    //24 * i8 :因為是8字節讀取,所以寫成3個指針
    var value1: UnsafeRawPointer
    var value2: UnsafeRawPointer
    var value3: UnsafeRawPointer
    //type 存放metadata,目的是為了找到Value Witness Table 值目錄表
    var type: UnsafeRawPointer
    // i8* 存放pwt指針
    var pwt: UnsafeRawPointer
}

// 內存重綁定
withUnsafePointer(to: &rectangle) { ptr in
    ptr.withMemoryRebound(to: protocolData.self, capacity: 1) { pointer in
        print(pointer.pointee)
    }
}

withUnsafePointer(to: &rectangle1) { ptr in
    ptr.withMemoryRebound(to: protocolData.self, capacity: 1) { pointer in
        print(pointer.pointee)
    }
}

<!--打印結果-->
protocolData(value1: 0x000000010683bac0, value2: 0x0000000000000000, value3: 0x0000000000000000, type: 0x00000001000040c0, pwt: 0x0000000100004050)
protocolData(value1: 0x000000010683bac0, value2: 0x0000000000000000, value3: 0x0000000000000000, type: 0x00000001000040c0, pwt: 0x0000000100004050)

此時我們看到打印結果是一樣的。

那么修改呢?

添加如下代碼:

protocol Shape {
    // 為了方便修改,在這聲明一下
    var width: Double {get set}
    var area: Double {get}
}

rectangle1.width = 50
image

通過lldb重新打印,我們可以看到在修改值后,內存地址已經修改了,此時就是寫時復制。當復制時并沒有值的修改,所以兩個變量指向同一個堆區內存。當修改變量的時候,會原本的堆區內存的值拷貝到一個新的內存區域,并進行值的修改。

如果我們將struct修改成class,這里并不會觸發寫時復制,因為在Swift中類是引用類型,修改類的值就是修改其引用地址中的值。這里就不驗證了,感興趣的可以自己去試試。

如果我們將Double換成String原理也是一致的,這里也就不一一驗證了。

3.5.3 小結

至此我們也就清楚了,為什么協議中通過witness_method調用,最終能找到V-Table中的方法,原因就是存儲了metadatapwt。這也是我們都聲明為協議類型,最終能打印出不同形狀的面積根本原因。

4. 總結

至此我們對Swift中協議的分析就結束了,現總結如下:

  1. Swift中類、結構體、枚舉都可以遵守協議
  2. 遵守多個協議使用逗號(,)分隔
  3. 有父類的,父類寫在前面,協議在后面用逗號(,)分隔
  4. 協議中可以添加屬性
    1. 屬性可以是實例屬性和類型屬性
    2. 屬性需要使用var修飾,不能屬于let
    3. 類型屬性只能使用static修飾,不能使用class
    4. 我們需要聲明屬性必須是可讀的或者可讀可寫的
  5. 協議中可以添加方法
    1. 可以是實例方法或類方法
    2. 像普通方法一樣放在協議定義中,但不需要大括號和方法體
    3. 協議中不支持為協議中的方法提供默認參數
    4. 協議中的類方法也只能使用static關鍵字作為前綴,不能使用class
    5. 可以使用mutating提供異變方法,以使用該方法時修改實體的屬性等。
    6. 可以定義構造方法,但是使用的時候需要使用required關鍵字
  6. 如果定義由類專屬協議,則需要繼承自AnyObject
  7. 協議可以作為類型
    1. 作為函數、方法或構造器中的參數類型或返回值類型
    2. 作為常量、變量或屬性的類型
    3. 作為數組、字典或其他容器中的元素類型
  8. 協議的底層存儲結構是:24字節的ValueBuffer+ metadata(8字節,也就是vwt) + pwt(8字節)
    1. 前24字節,官方說法是ValueBuffer,主要用于存儲遵循了協議的實體的屬性值
    2. 如果超過ValueBuffer最大容量就會開辟內存進行存儲,此24字節拿出8字節存儲指向該內存區域的指針
    3. 目前對于類,發現其存儲的都是指針
    4. 存儲metadata是為了查找遵守協議的實體中實現協議的方法
    5. pwt就是protocol witness table協議目擊表,存儲協議中的方法
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,533評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,055評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,365評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,561評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,346評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,889評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,978評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,118評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,637評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,558評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,739評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,246評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,980評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,619評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,347評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,702評論 2 370