前言
本篇文章将分析Swift中最后一个重要的知识点 👉 泛型
,首先介绍一下概念,然后讲解常用基础的语法,最后重点分析下泛型函数
,主要是从IR代码层面
分析下泛型函数的调用流程
。
一、泛型的概念
首先说一下泛型的概念 👇
- 泛型代码能根据所定义的要求写出可以用于
任何类型
的灵活的、可复用的函数
。可以编写出可复用、意图表达清晰、抽象的
代码。 - 泛型是
Swift
最强大的特性之一,很多Swift标准库
是基于泛型代码
构建的。
例如,Swift
的Array
和Dictionary
类型都是泛型集合
。可以创建一个容纳Int 值
的数组,或者容纳String 值
的数组,甚至容纳任何 Swift 可以创建的其他类型
的数组。同样,可以创建一个存储任何指定类型值的字典
,而且类型没有限制
。 - 泛型所解决的问题:
代码的复用性和抽象能力
。
比如交换两个值,这里的值可以是Int、Double、String
。
例如下面的例子,其中的T就是泛型👇
func test<T>(_ a: T, _ b: T)->Bool{
return a == b
}
//经典例子swap,使用泛型,可以满足不同类型参数的调用
func swap<T>(_ a: inout T, _ b: inout T){
let tmp = a
a = b
b = tmp
}
二、泛型的基础语法
接着我们来看看泛型的基础用法,主要讲3点👇
- 类型约束
- 关联类型
- Where语句
2.1 类型约束
在一个类型参数后面
放置协议或者是类
,例如下面的例子,要求类型参数T遵循Equatable协议
👇
func test<T: Equatable>(_ a: T, _ b: T)->Bool{
return a == b
}
2.2 关联类型
在定义协议时,使用关联类型给协议中用到的类型起一个占位符
名称。关联类型只能用于协议,并且是通过关键字associatedtype
指定。
首先我们来看看下面这个示例,仿写的一个栈
的结构体👇
struct LGStack {
private var items = [Int]()
mutating func push(_ item: Int){
items.append(item)
}
mutating func pop() -> Int?{
if items.isEmpty {
return nil
}
return items.removeLast()
}
}
该结构体中有个成员Item
,是个数组
,当前只能存储Int类型
的数据,如果想使用其他类型呢?👉 可以通过协议
来实现 👇
protocol LGStackProtocol {
//协议中使用类型的占位符
associatedtype Item
}
struct LGStack: LGStackProtocol{
//在使用时,需要指定具体的类型
typealias Item = Int
private var items = [Item]()
mutating func push(_ item: Item){
items.append(item)
}
mutating func pop() -> Item?{
if items.isEmpty {
return nil
}
return items.removeLast()
}
}
此时在协议LGStackProtocol
中就用到了associatedtype
关键字,先让Item占个位,然后在类LGStack遵循协议后使用typealias
关键字指定
Item的具体类型
。当然,我们这个时候也可以写一个泛型的版本👇
struct LGStack<Element> {
private var items = [Element]()
mutating func push(_ item: Element){
items.append(item)
}
mutating func pop() -> Element?{
if items.isEmpty {
return nil
}
return items.removeLast()
}
}
2.3 Where语句
where语句主要用于表明泛型需要满足的条件
,即限制形式参数
的要求👇
protocol LGStackProtocol {
//协议中使用类型的占位符
associatedtype Item
var itemCount: Int {get}
mutating func pop() -> Item?
func index(of index: Int) -> Item
}
struct LGStack: LGStackProtocol{
//在使用时,需要指定具体的类型
typealias Item = Int
private var items = [Item]()
var itemCount: Int{
get{
return items.count
}
}
mutating func push(_ item: Item){
items.append(item)
}
mutating func pop() -> Item?{
if items.isEmpty {
return nil
}
return items.removeLast()
}
func index(of index: Int) -> Item {
return items[index]
}
}
/*
where语句
- T1.Item == T2.Item 表示T1和T2中的类型必须相等
- T1.Item: Equatable 表示T1的类型必须遵循Equatable协议,意味着T2也要遵循Equatable协议
*/
func compare<T1: LGStackProtocol, T2: LGStackProtocol>(_ stack1: T1, _ stack2: T2) -> Bool where T1.Item == T2.Item, T1.Item: Equatable{
guard stack1.itemCount == stack2.itemCount else {
return false
}
for i in 0..<stack1.itemCount {
if stack1.index(of: i) != stack2.index(of: i){
return false
}
}
return true
}
还可以这么写👇
extension LGStackProtocol where Item: Equatable{}
- 当希望
泛型
指定类型时拥有特定功能
,可以这么写👇(在上述写法的基础上增加extension)
extension LGStackProtocol where Item == Int{
func test(){
print("test")
}
}
var s = LGStack()
s.test()
其中的test()
就是你自定义的功能
。
注意:如果将where后的
Int
改成Double
类型,是无法找到
test函数的!
三、泛型函数
我们在上面介绍了泛型的基本语法
,接下来我们来分析下泛型的底层原理
。先看示例👇
//简单的泛型函数
func testGenric<T>(_ value: T) -> T{
let tmp = value
return tmp
}
class LGTeacher {
var age: Int = 18
var name: String = "Kody"
}
//传入Int类型
testGenric(10)
//传入元组
testGenric((10, 20))
//传入实例对象
testGenric(LGTeacher())
从上面的代码中可以看出,泛型函数可以接受任何类型
。那么问题来了👇
泛型是如何
区分不同的参数
,来管理不同类型的内存
呢?
老办法,查看IR代码👇
至此我们知道,当前泛型通过VWT
来进行内存操作
。
3.1 VWT
看下VWT
的源码(在Metadata.h
中TargetValueWitnessTable
)👇
/// A value-witness table. A value witness table is built around
/// the requirements of some specific type. The information in
/// a value-witness table is intended to be sufficient to lay out
/// and manipulate values of an arbitrary type.
template <typename Runtime> struct TargetValueWitnessTable {
// For the meaning of all of these witnesses, consult the comments
// on their associated typedefs, above.
#define WANT_ONLY_REQUIRED_VALUE_WITNESSES
#define VALUE_WITNESS(LOWER_ID, UPPER_ID) \
typename TargetValueWitnessTypes<Runtime>::LOWER_ID LOWER_ID;
#define FUNCTION_VALUE_WITNESS(LOWER_ID, UPPER_ID, RET, PARAMS) \
typename TargetValueWitnessTypes<Runtime>::LOWER_ID LOWER_ID;
#include "swift/ABI/ValueWitness.def"
using StoredSize = typename Runtime::StoredSize;
/// Is the external type layout of this type incomplete?
bool isIncomplete() const {
return flags.isIncomplete();
}
/// Would values of a type with the given layout requirements be
/// allocated inline?
static bool isValueInline(bool isBitwiseTakable, StoredSize size,
StoredSize alignment) {
return (isBitwiseTakable && size <= sizeof(TargetValueBuffer<Runtime>) &&
alignment <= alignof(TargetValueBuffer<Runtime>));
}
/// Are values of this type allocated inline?
bool isValueInline() const {
return flags.isInlineStorage();
}
/// Is this type POD?
bool isPOD() const {
return flags.isPOD();
}
/// Is this type bitwise-takable?
bool isBitwiseTakable() const {
return flags.isBitwiseTakable();
}
/// Return the size of this type. Unlike in C, this has not been
/// padded up to the alignment; that value is maintained as
/// 'stride'.
StoredSize getSize() const {
return size;
}
/// Return the stride of this type. This is the size rounded up to
/// be a multiple of the alignment.
StoredSize getStride() const {
return stride;
}
/// Return the alignment required by this type, in bytes.
StoredSize getAlignment() const {
return flags.getAlignment();
}
/// The alignment mask of this type. An offset may be rounded up to
/// the required alignment by adding this mask and masking by its
/// bit-negation.
///
/// For example, if the type needs to be 8-byte aligned, the value
/// of this witness is 0x7.
StoredSize getAlignmentMask() const {
return flags.getAlignmentMask();
}
/// The number of extra inhabitants, that is, bit patterns that do not form
/// valid values of the type, in this type's binary representation.
unsigned getNumExtraInhabitants() const {
return extraInhabitantCount;
}
/// Assert that this value witness table is an enum value witness table
/// and return it as such.
///
/// This has an awful name because it's supposed to be internal to
/// this file. Code outside this file should use LLVM's cast/dyn_cast.
/// We don't want to use those here because we need to avoid accidentally
/// introducing ABI dependencies on LLVM structures.
const struct EnumValueWitnessTable *_asEVWT() const;
/// Get the type layout record within this value witness table.
const TypeLayout *getTypeLayout() const {
return reinterpret_cast<const TypeLayout *>(&size);
}
/// Check whether this metadata is complete.
bool checkIsComplete() const;
/// "Publish" the layout of this type to other threads. All other stores
/// to the value witness table (including its extended header) should have
/// happened before this is called.
void publishLayout(const TypeLayout &layout);
};
很明了,VWT
中存放的是 size(大小)、alignment(对齐方式)、stride(步长)
,大致结构图👇
所以metadata
中都存放了VWT
来管理类型的值。比如Int、String、Class的复制销毁
、创建
以及是否需要引用计数
。
再回过头来看看上面示例的IR代码,其实执行的流程大致如下👇
- 询问
metadata
中VWT:size,stride
分配内存空间 - 初始化
temp
- 调用
VWT-copy
方法拷贝值到temp
- 返回
temp
- 调用
VWT-destory
方法销毁局部变量
所以👇
泛型
在整个运行过程中的关键依赖于metadata
。
3.2 源码调试
主要分为2类调试:值类型和引用类型。
3.2.1 值类型的调试
首先打上断点👇
打开汇编👇
运行👇
然后,我们去swift源码中查找NativeBox
(在metadataimpl.h源码中)👇
对于
值类型
通过内存copy和move
进行内存处理。
3.2.2 引用类型的调试
同理,引用类型也是先打上断点,查看汇编 👇
/// A box implementation class for Swift object pointers.
struct SwiftRetainableBox :
RetainableBoxBase<SwiftRetainableBox, HeapObject*> {
static HeapObject *retain(HeapObject *obj) {
if (isAtomic) {
swift_retain(obj);
} else {
swift_nonatomic_retain(obj);
}
return obj;
}
static void release(HeapObject *obj) {
if (isAtomic) {
swift_release(obj);
} else {
swift_nonatomic_release(obj);
}
}
};
SwiftRetainableBox继承RetainableBoxBase👇
/// A CRTP base class for defining boxes of retainable pointers.
template <class Impl, class T> struct RetainableBoxBase {
using type = T;
static constexpr size_t size = sizeof(T);
static constexpr size_t alignment = alignof(T);
static constexpr size_t stride = sizeof(T);
static constexpr bool isPOD = false;
static constexpr bool isBitwiseTakable = true;
#ifdef SWIFT_STDLIB_USE_NONATOMIC_RC
static constexpr bool isAtomic = false;
#else
static constexpr bool isAtomic = true;
#endif
static void destroy(T *addr) {
Impl::release(*addr);
}
static T *initializeWithCopy(T *dest, T *src) {
*dest = Impl::retain(*src);
return dest;
}
static T *initializeWithTake(T *dest, T *src) {
*dest = *src;
return dest;
}
static T *assignWithCopy(T *dest, T *src) {
T oldValue = *dest;
*dest = Impl::retain(*src);
Impl::release(oldValue);
return dest;
}
static T *assignWithTake(T *dest, T *src) {
T oldValue = *dest;
*dest = *src;
Impl::release(oldValue);
return dest;
}
// Right now, all object pointers are brought down to the least
// common denominator for extra inhabitants, so that we don't have
// to worry about e.g. type substitution on an enum type
// fundamentally changing the layout.
static constexpr unsigned numExtraInhabitants =
swift_getHeapObjectExtraInhabitantCount();
static void storeExtraInhabitantTag(T *dest, unsigned tag) {
swift_storeHeapObjectExtraInhabitant((HeapObject**) dest, tag - 1);
}
static unsigned getExtraInhabitantTag(const T *src) {
return swift_getHeapObjectExtraInhabitantIndex((HeapObject* const *) src) +1;
}
};
所以,引用类型的处理中也包含了destroy
initializeWithCopy
和initializeWithTake
。再回过头来看👇
所以👇
对于引用类型,会调用
retain
进行引用计数+1
,处理完在调用destory
,而destory
中是调用release
进行引用计数-1
。
小结
-
对于一个
值类型
,例如Integer
👇1、该类型的copy和move操作会进行内存拷贝
2、destory操作则不进行任何操作 -
对于一个
引用类型
,如class👇1、该类型的copy操作会对引用计数+1,
2、move操作会拷贝指针,而不会更新引用计数
3、destory操作会对引用计数-1
3.3 方法作为类型
还有一种场景 👉 如果把一个方法
当做泛型类型
传递进去呢?例如👇
func makeIncrementer() -> (Int) -> Int {
var runningTotal = 10
return {
runningTotal += $0
return runningTotal
}
}
func test<T>(_ value: T) {
}
let makeInc = makeIncrementer()
test(makeInc)
我们还是看IR👇
流程并不复杂,我们可以通过内存绑定
仿写这个过程👇
仿写
struct HeapObject {
var type: UnsafeRawPointer
var refCount1: UInt32
var refcount2: UInt32
}
struct Box<T> {
var refCounted:HeapObject
var value: T //捕获值
}
struct FunctionData<BoxType> {
var ptr: UnsafeRawPointer //内嵌函数地址
var captureValue: UnsafePointer<BoxType>? //捕获值地址
}
struct TestData<T> {
var ref: HeapObject
var function: FunctionData<T>
}
func makeIncrementer() -> (Int) -> Int {
var runningTotal = 10
return {
runningTotal += $0
return runningTotal
}
}
func test<T>(_ value: T) {
let ptr = UnsafeMutablePointer<T>.allocate(capacity: 1)
ptr.initialize(to: value)
//对于泛型T来说做了一层TestData桥接,目的是为了能够更好的解决不同值传递
let ctx = ptr.withMemoryRebound(to: FunctionData<TestData<Box<Int>>>.self, capacity: 1) {
$0.pointee.captureValue?.pointee.function.captureValue!
}
print(ctx?.pointee.value)
ptr.deinitialize(count: 1)
ptr.deallocate()
}
//{i8 *, swift type *}
let makeInc = makeIncrementer()
test(makeInc)
运行👇
对于泛型T
来说做了一层TestData桥接
,目的是为了能够更好的解决不同值传递
。
总结
本篇文章重点分析了Swift泛型
的基础语法
和IR底层的处理流程
,分别分析了值类型
、引用类型
和函数入参的场景
,希望大家能够掌握。至此,Swift的知识点均已覆盖完毕,感谢大家的支持!
网友评论