In addition to Weibo, there is also WeChat
Please pay attention
WeChat public account
Shulou
2025-01-18 Update From: SLTechnology News&Howtos shulou NAV: SLTechnology News&Howtos > Development >
Share
Shulou(Shulou.com)06/03 Report--
This article focuses on "how to understand the protocols in Swift". Interested friends may wish to have a look at it. The method introduced in this paper is simple, fast and practical. Let's let the editor take you to learn how to understand the protocols in Swift.
1. Preface
The protocol defines a blueprint that specifies the methods, properties, and other things needed to implement a particular task or function. Classes, structures, and enumerations can all follow the protocol and provide a concrete implementation of these requirements defined by the protocol. If a type can meet the requirements of a protocol, it can be said that the type follows the protocol.
In addition to following the requirements that the type of the protocol must achieve, the protocol can also be extended to achieve part of the requirements or to achieve some additional functions, which can be used by these protocol-compliant types.
two。 Basic usage of ▐ 2.1Protocol syntax
The definition of a protocol is very similar to the definition of classes, structures, and enumerations.
1. Basic grammar
Protocol SomeProtocol {/ / here is the definition part of the protocol}
2. If you want a custom type to follow a protocol, when defining the type, you need to add the protocol name after the type name, separated by a colon (:). If you need to follow multiple protocols, the protocols are separated by commas (,):
Struct SomeStructure: FirstProtocol, AnotherProtocol {/ / here is the definition part of the structure}
3. If the custom type has a parent class, the parent class name should be placed before the protocol name, separated by a comma:
Class SomeClass: SomeSuperClass, FirstProtocol, AnotherProtocol {/ / here is the definition part of the class} ▐ 2.2 attribute requirements
We can add attributes to the protocol, but we need to note the following:
Properties can be instance properties and type properties
Property needs to be decorated with var and cannot belong to let
Type properties can only be decorated with static, not class
We need to declare that the property must be readable or writable
Protocol SomeProtocol {var propertyOne: Int {get set} var propertyTwo: Int {get} static var propertyThree: Int {get set}} ▐ 2.3 method requirements
We can add methods to the protocol, but we need to note the following:
Can be an instance method or a class method
Put in the protocol definition as usual, but do not need curly braces and method body
Default parameters for methods in the protocol are not supported in the protocol
Class methods in the protocol can only be prefixed with the static keyword, not class.
You can use mutating to provide a mutation method to modify the attributes of an entity, etc.
You can define the constructor, but you need to use the required keyword when using it
Protocol SomeProtocol {func someMethod1 () func someMethod2 ()-> Int}
Construction method
Protocol SomeProtocol {init (param: Int)} class SomeClass: SomeProtocol {required init (param: Int) {}}
Mutation method
Protocol Togglable {mutating func toggle ()} enum OnOffSwitch: Togglable {case off, on mutating func toggle () {switch self {case .off: self = .on case .on: self = .off} ▐ 2.4 protocol as a type
Although the protocol itself does not implement any function, the protocol can be used as a fully functional type. Protocols are used as types, sometimes referred to as "existential types". The term implies the existence of a type T that follows the protocol T.
The protocol can be used like other common types. The scenarios are as follows:
As a parameter type or return value type in a function, method, or constructor
As a type of constant, variable, or attribute
As an element type in an array, dictionary, or other container
Protocol SomeProtocol {} class SomeClass {required init (param: SomeProtocol) {}} ▐ 2.5 other
The protocol can also be inherited
You can follow the protocol in the extension
Declare the adoption of the agreement in the extension
Use composition to adopt protocols
Can be defined by class proprietary protocols, only need to inherit from AnyObject
Protocols can be synthesized.
The protocol can also be extended.
3. Method invocation in the protocol
For example, in mathematics, we will find the area of a figure, but the formula for calculating the area of different shapes is different, how can we achieve it with code?
First of all, we can do this by inheriting the method of the parent class, but here we can use the protocol to do so:
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 var rectangle: Shape = Rectangle.init (10.0,20.0) print (circle.area) print (rectangle.area) 314.0 200.0
The print result at this time is in line with our expectations.
We know that the protocol can be extended, so we modify the code of the protocol as follows:
Protocol Shape {/ / var area: Double {get}} extension Shape {var area: Double {get {return 0.0} 0.0 0.0
It is not printed as we expected at this time, but what if we declare the variable as follows:
Var circle: Circle = Circle.init (10.0) var rectangle: Rectangle = Rectangle.init (10.0,20.0) 314.0 200.0
The printing at this time is in line with our expectations.
In fact, we can clearly understand why 0.0 is printed. In this article on Swift method scheduling, we introduced that the method declared in extension is called statically, that is, after compilation, the address of the current code has been determined and we cannot modify it. When declared as a Shap type, the default call is the get method of the property in Shape extension. Let's verify it with sil code. Please refer to my previous article on how to generate sil code.
For ease of viewing, we simplify and modify the code as follows:
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 var a = circle.area
The generated sil code:
We can clearly see from the sil code that the Shape.area.getter method is called directly here.
Let's take a look at it again with some simple code:
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
You can see that the print result of the above code is all Person eat, so why print the same result? First of all, we can see from the code that the eat method is declared in PersonProtocol. For declared protocol methods, if they are also implemented in the class, the methods in the protocol extension are not called. In the above example, the attribute is not declared in the protocol, but a property is added to the protocol extension. Let's take a look at the sil code of the above code:
First of all, we can see that there is a real difference between the two eat methods. The variable declared as a protocol type first calls the eat method through witness_method, and the other through class_method.
Witness_method obtains the corresponding function address through PWT (Protocol Witness Table)
Class_method is called through the function table of the class to find the function.
We can find sil_witness_table in the sil code just now, where there is a PersonProtocol.eat method, and if we find the PersonProtocol.eat method, we can find that it is the Person.eat method that calls the VTable in the class that class_method is looking for.
If we do not declare the eat method in the agreement:
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
View the sil code:
At this point, we can see that the method is still called directly (static call) when the method is not declared in the protocol.
So for the scheduling of methods in the protocol:
For methods that are not declared in the agreement
The implementation in the protocol extension is to call the
It is decided according to its scheduling mode in the entities that follow the protocol.
Both are implemented. If the declared instance is a protocol type, the method in the protocol extension is called directly, and otherwise, the method in the protocol entity is called.
For methods declared in the agreement
If the entity that follows the protocol implements the method, the implemented method is found through the PWT protocol eyewitness table to call (regardless of the type of the declared variable)
If the entity that follows the protocol is not implemented and the protocol extension is implemented, the method in the protocol extension is called.
4. Exploration of the principle of Protocol
When exploring method calls in the protocol above, we mentioned that PWT is also known as Protocol witness table, the protocol visual table, so where is it stored? As we said in this article on Swift method scheduling, V-Table is stored in metadata, so let's explore the storage location of PWT.
▐ 4.1memory footprint
First of all, let's take a look at the printed result of the following code:
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 (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 ▐ 4.2 lldb explore memory structure
The first thing I can think of when I see this print result is that life will store more information for the protocol type. When life is a class, it stores 8 bytes of the pointer to the instance object of the class. Let's explore what information is stored in these 40 bytes through lldb debugging.
▐ 4.3 sil explores memory structure
Through lldb, we can see that some information should be stored inside it, so what exactly is stored? We are looking at the sil code:
We can see in the sil code that init_existential_addr is used when initializing the variable circle. Look at the SIL document:
Initialize the memory of the% 0 reference with a part of the presence container that is ready to contain the type $T. The result of this instruction is an address that refers to the storage space for the contained values, which is still not initialized. The included value must be stored as-d or copy_addr-ed in order to fully initialize the existing value. If there is a container whose value needs to be destroyed when it is not initialized, you must use deinit_existential_addr to do so. You can use destroy_addr to destroy a fully initialized existence container as usual. Destroying an addr that partially initializes the presence of a container is an undefined behavior.
What the document means is that an existential container containing $T is used to initialize the memory referenced by% 0. In this case, the existential container containing Circle is used to initialize the memory referenced by circle, which simply wraps the circle into an existential container-initialized memory.
Existential container is a special data type generated by the compiler and is also used to manage protocol types that comply with the same protocol. Because these plasticizer types have different memory space sizes, storage consistency can be achieved by using existential container for management.
▐ 4.4IR Code explores memory structure
So what does this existential container pack? You can't see anything from the sil code right now, so let's take a look at the IR code:
; a structure, an array of 24 bytes of memory, wift.type pointer, i8* pointer% T4main5ShapeP = type {[24 x i8],% swift.type*, i8memory *} define i32 @ main (i32% 0, i8memory *% 1) # 0 {entry:% 2 = bitcast i8memory *% 1 to i8* Main.Circle 's metadata% 3 = call swiftcc% swift.metadata_response @ "type metadata accessor for main.Circle" (i640) # 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) Save% 4, that is, metadata, in the T4main5ShapeP structure, where the location is store% swift.type*% 4,% swift.type** getelementptr inbounds (% T4main5ShapeP,% T4main5ShapeP* @ "main.circle: main.Shape", i320, i321), align 8 Save pwt, which is the protocol visual table, to the third location store i8 protocol witness table for main.Circle * getelementptr inbounds ([2 x i8 *], [2 x i8 *] * @ "protocol witness table for main.Circle: main.Shape in main", i320, i320), i8 memory * getelementptr inbounds (% T4main5ShapeP,% T4main5ShapeP* @ "main.circle: main.Shape", i320, i32 2), align 8 Store% 5 to the secondary pointer,% 5 is the object from init, so here is a HeapObject structure, that is, store% T4main6CircleC*% 5,% T4main6cle6CircleC* bitcast (% T4main5ShapeP* @ "main.circle: main.Shape" to% T4main6CircleClearing *), align 8} at the first 8-byte memory space of the T4main6CircleC structure.
From the IR code, we can see that the storage in this is a structure, which is mainly divided into three aspects:
A continuous 24-byte space
A pointer to store the metadata
Store pwt pointers
▐ 4.5 imitation
Let's imitate this structure:
Struct HeapObject {var type: UnsafeRawPointer var refCount1: UInt32 var refCount2: UInt32} struct protocolData {/ / 24 * i8: because it is an 8-byte read, it is written as three pointers var value1: UnsafeRawPointer var value2: UnsafeRawPointer var value3: UnsafeRawPointer / / type to store metadata. The purpose is to find the Value Witness Table value directory table var type: UnsafeRawPointer / / i8* to store pwt pointers var pwt: UnsafeRawPointer} 4.5.1 classes follow the protocol rebinding
Perform memory rebinding:
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 / / convert circle into protocolData structure withUnsafePointer (to: & circle) {ptr in ptr.withMemoryRebound (to: protocolData.self)
Lldb
View through lldb:
We can also see the corresponding HeapObject structure.
This structure stores instance variables of Circle
And the metadata in this is the same as the address of the stored metadata in protocolData.
Looking at the pointer corresponding to pwt through the cat address command, you can see that this memory corresponds to the protocol witness table of SwiftProtocol.Circle.
At this point, we have clearly found the storage location of your PWT, and the PWT is in the memory structure of the protocol type instance.
4.5.2 the structure follows the protocol rebinding
In the above example, we are using classes. We know that classes are reference types. What if we replace them with structures?
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 Struct HeapObject {var type: UnsafeRawPointer var refCount1: UInt32 var refCount2: UInt32} struct protocolData {/ / 24 * i8: because it is an 8-byte read So write three pointers var value1: UnsafeRawPointer var value2: UnsafeRawPointer var value3: UnsafeRawPointer / / type to store the metadata The purpose is to find the Value Witness Table value catalog table var type: UnsafeRawPointer / / i8* to store the pwt pointer var pwt: UnsafeRawPointer} / / to convert circle into a protocolData structure 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)
Take a look at the IR code:
Define i32 @ main (i32% 0, i8 percent *% 1) # 0 {entry:% 2 = bitcast i8 percent *% 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 (, * @ "full type metadata for main.Rectangle", i320, i321) to% swift.type*),% swift.type** getelementptr inbounds (% T4main5ShapeP,% T4main5ShapeP* @ "main.rectangle: main.Shape", i320, i321), align 8 Pwt store i8 main.rectangle * getelementptr inbounds ([2 x i8 *], [2 x i8 *] * @ "protocol witness table for main.Rectangle: main.Shape in main", i320, i320), i8 benchmark * getelementptr inbounds (% T4main5ShapeP,% T4main5ShapeP* @ "main.rectangle: main.Shape", i320, i322), i8 4 is 10 store double 4, double* getelementptr inbounds (% T4main9RectangleV,% T4main9RectangleV* bitcast (% T4main5ShapeP* @ "main.Shape" to% T4main9RectangleV*), i320, i320, i320), align 8; storage 5 is 20 store double 5, double* getelementptr inbounds (% T4main9RectangleV,% T4main9RectangleV* bitcast (% T4main5ShapeP* @ "main.Shape" to% T4main9RectangleV*), i320, i321, i320), align 8}
Through the IR code, we can see:
The storage for metadata and pwt remains the same.
What if there are three attributes?
Struct Rectangle: Shape {var width, width2, height: Double init (_ width: Double, _ width2: Double, _ height: Double) {self.width = width self.width2 = width2 self.height = height} var area: Double {get {return width * height} protocolData (value1: 0x4024000000000000, value2: 0x4034000000000000, value3: 0x403e000000000000, type: 0x0000000100004098, pwt: 0x0000000100004028)
The values of the three Value are 10, 10, 20, 30, respectively.
What if it's four?
Struct Rectangle: Shape {var width, width2, height, height1: Double init (_ width: Double, _ width2: Double, _ height: Double, _ height1: Double) {self.width = width self.width2 = width2 self.height = height self.height1 = height1} var area: Double {get {return width * height} var rectangle: Shape = Rectangle 20.0,30.0,40.0) protocolData (value1: 0x0000000100715870, value2: 0x0000000000000000, value3: 0x0000000000000000, type: 0x00000001000040c0, pwt: 0x0000000100004050)
At this point, you don't see the Double value directly. Check the memory of value1:
At this point, we can see that the four values of 10, 10, 20, 30, 40 are stored in this memory.
So if we need to store more than 24 x i8 bytes, that is, 24 bytes, we will open up memory space for storage.
The order here is to open up memory space, store values, and record pointers if there is not enough storage. Instead of not storing enough memory space in the first place.
We all know that the structure is a value type, if more than 24 bytes of storage space will open up memory to store the values in the structure, if a copy occurs at this time, what will be the structure? Let's check it out:
Copy of structure:
Protocol Shape {var area: Double {get}} struct Rectangle: Shape {var width, width2, height, height1: Double init (_ width: Double, _ width2: Double, _ height: Double _ height1: Double) {self.width = width self.width2 = width2 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: because it is 8 byte read So write three pointers var value1: UnsafeRawPointer var value2: UnsafeRawPointer var value3: UnsafeRawPointer / / type to store the metadata The purpose is to find the Value Witness Table value catalog table var type: UnsafeRawPointer / / i8* to store the pwt pointer var pwt: UnsafeRawPointer} / / memory rebinding 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)
At this point we see that the print result is the same.
What about the revision?
Add the following code:
Protocol Shape {/ / to facilitate modification, declare here that var width: Double {get set} var area: Double {get}} rectangle1.width = 50
Through lldb reprinting, we can see that the memory address has been changed after the value has been modified, which is copied on write. There is no change in the value when copied, so the two variables point to the same heap memory. When you modify a variable, the value of the original heap memory is copied to a new memory area, and the value is modified.
If we change struct to class, copy on write will not be triggered here, because in Swift, a class is a reference type, and to change the value of a class is to change the value in its reference address.
If we replace Double with String principle is the same, it will not be verified here one by one.
4.5.3 Summary
At this point, we know why the method in V-Table can finally be found through witness_method calls in the protocol, because metadata and pwt are stored. This is the fundamental reason why we are all declared as protocol types and will eventually be able to print areas of different shapes.
5. Summary
At this point, our analysis of the protocols in Swift is over, and it is summarized as follows:
Classes, structures and enumerations in 1.Swift can all abide by the protocol.
two。
3. If there is a parent class, the parent class is written first, and the protocol is separated by a comma (,).
4. Attributes can be added to the protocol
Properties can be instance properties and type properties
Property needs to be decorated with var and cannot belong to let
Type properties can only be decorated with static, not class
We need to declare that the property must be readable or writable
5. Methods can be added to the protocol
Can be an instance method or a class method
Put in the protocol definition as usual, but do not need curly braces and method body
Default parameters for methods in the protocol are not supported in the protocol
Class methods in the protocol can only be prefixed with the static keyword, not class.
You can use mutating to provide a variant method to modify the properties of an entity, and so on.
You can define the constructor, but you need to use the required keyword when using it
6. If the definition is made by a class proprietary protocol, it needs to be inherited from AnyObject
7. Protocol can be used as a type
As a parameter type or return value type in a function, method, or constructor
As a type of constant, variable, or attribute
As an element type in an array, dictionary, or other container
8. The underlying storage structure of the protocol is: 24 bytes of ValueBuffer+ metadata (8 bytes, i.e. vwt) + pwt (8 bytes)
The first 24 bytes, officially known as ValueBuffer, are mainly used to store the attribute values of entities that follow the protocol.
If the maximum capacity of the ValueBuffer is exceeded, memory will be opened up for storage. The 24 bytes will take out 8 bytes to store pointers to the memory area.
At present, for classes, it is found that all they store are pointers.
Metadata is stored to find a way to implement the protocol in an entity that complies with it
Pwt is the visual table of protocol witness table protocol and the method of storing protocol.
At this point, I believe you have a deeper understanding of "how to understand the protocols in Swift". You might as well do it in practice. Here is the website, more related content can enter the relevant channels to inquire, follow us, continue to learn!
Welcome to subscribe "Shulou Technology Information " to get latest news, interesting things and hot topics in the IT industry, and controls the hottest and latest Internet news, technology news and IT industry trends.
Views: 0
*The comments in the above article only represent the author's personal views and do not represent the views and positions of this website. If you have more insights, please feel free to contribute and share.
Continue with the installation of the previous hadoop.First, install zookooper1. Decompress zookoope
"Every 5-10 years, there's a rare product, a really special, very unusual product that's the most un
© 2024 shulou.com SLNews company. All rights reserved.