Swift & Cocoa 实战之泛型编程:Swift 中的泛型编程

Generics in Swift

Swift 中的泛型编程

本文中,笔者旨在对于Swift中的泛型编程进行一个综合性描述。读者可以查看上一篇 系列中的描述来看之前笔者的论文。泛型编程是编程方式的一种,主要是一些譬如类、结构体以及枚举这样的复杂类型以及函数可以使用类型参数进行定义(type parameters)。类型参数可以看做是真实类型的占位符,当泛型或者函数被使用时才会被真实类型替换。

在Swift中,对于泛型编程最直观的表现当属Array这个类型。在Objective-C中,Array的示例NSArray可以包含任意类型的对象。但是,Swift中的Array类型需要在声明时即指定它们包含的元素类型,,譬如Array<Int>,Array<UIView>。Array与Int都是一种数据类型,泛型机制允许这两种类型有机组合起来从而能够传递更多的额外的信息。

备注:在Swift中惯用的声明Array包含了Foo类型的用法是[Foo]这种语法,而本文中使用Array<Foo>写法旨在帮助理解以及强调Array是一个通用泛型。

Why generics (为什么使用泛型)?

这里列举出几个静态类型语言中使用泛型编程的原因:

  • 类型安全:类似于Array这样的容器类型,可以给出它们存放的具体的元素的类型,从而告知编译器可以插入容器中的对象的类型以及从容器中返回的对象的类型。这种机制作用于所有可以被当做Array参数的类型,并且也作用于类型之间的关系。

  • 减少冗余代码:有些情况下需要对多个数据类型进行相同的操作,可以用一个泛型方程来代替多个不同类型参数或者返回值的重复的方程。这样可以避免潜在的代码错误等等。

  • 灵活的依赖库:第三方库可以暴露一些接口,从而避免使用这些接口的开发者被强制使用或者类型或者返回值为一个固定的类型。它们可以使用泛型进行更加抽象地编程,举例来说,泛型会允许一个接口来接受不仅仅是一个Array参数,而是一个Collection参数。

Generic entities (泛型实体)

Swift中的泛型编程主要表现在以下两个不同的情况下:当定义某个类型或者定义某个函数时。泛型类型的特性以及<>这两个关键字往往意味着某个类型或者函数是泛型。

Generic types (泛型类型)

Swift中主要的三个用户可自定义的类型可以被当做泛型,下面就以Result枚举类型为例,该类型中存放了表征成功的Success以及表征失败的Failure:

enum Result<T,U> {
  case Success(T)
  case Failure(U)
}

在Result类型之后有两个类型参数: T以及U。这些类型参数将会在创建实例时被替换为真实的类型:

let aSuccess : Result<Int,String> = .Success(123)
let aFailure : Result<Int,String> = .Failure("temperature too high")

泛型类型的类型参数可以被用于以下的几个方面:

  • 作为属性的类型

  • 作为枚举体中的关联类型

  • 作为某个方法的返回值或者参数

  • 作为构造器的参数类型

  • 作为另一个泛型的类型参数,譬如Array

泛型可以在以下两种方式中被初始化:

  • 在创建新的实例时,类型参数被用户给出的真实的数据类型替换。

  • 类型被推导得出,通过调用初始化器或者静态方法来创建某个实例。

struct Queue<T> { /* ... */ }

// First way to instantiate
let a1 : Queue<Int> = Queue()
let a2 : Queue<Int> = Queue.staticFactoryMethod() // or just .staticFactoryMethod()

// Second way to instantiate
let b1 = Queue<Int>()
let b2 = Queue<Int>.staticFactoryMethod()

注意,像T这样的类型参数在类型定义时出现,无论这个类型何时被调用,这些类型参数都会被替换为真实的类型。举例而言,Array<String>或者Array<UIWindow>,便是以固定类型初始化的数组。而如果是像Array<T>这样的,它便一直处于由其他类型或者函数作为类型参数定义的上下文中。

Generic functions (泛型函数)

函数、方法、属性、下标以及初始化器都可以当做泛型进行处理,它们自身可以成为泛型或者存在于某个泛型类型的上下文中:

// Given an item,return an array with that item repeated n times
func duplicate<T>(item: T,numberOfTimes n: Int) -> [T] {
  var buffer : [T] = []
  for _ in 0 ..< n {
    buffer.append(item)
  }
  return buffer
}

同样的,类型参数是定义在<>紧跟在函数名之后。它主要可以用于以下几个方面:

  • 作为某个函数的参数

  • 作为函数的返回值

  • 作为另一个泛型类型的类型参数,譬如T可以作为Array<T?>中的一部分

当然,如果所有的类型参数都是未用状态编译器会不太友好。而泛型方法可以同时定义在泛型类型与非泛型类型上:

extension Result {
  // Transform the value of the 'Result' using one of two mapping functions
  func transform<V>(left: T -> V,right: U -> V) -> V {
    switch self {
    case .Success(let value): return left(value)
    case .Failure(let value): return right(value)
    }
  }
}

上述中的transform方法就是泛型方法,存放于泛型类型Result中。除了由Result中定义的类型参数T、U,泛型方法本身也定义了一个泛型参数V。当调用一个泛型方程时,不一定需要指明清楚传入的类型参数。编译器的类型推导接口会自动根据参数与返回类型推导出相关信息:

// Example use of the 'duplicate:numberOfTimes:' function defined earlier.
// T is inferred to be Int.
let a = duplicate(52,numberOfTimes: 10)

实际上,尝试去清楚地设置参数类型还会引发错误:

// Does not compile
// Error: "Cannot explicitly specialize a generic function"
let b = duplicate<String>("foo",numberOfTimes: 5)

Associated types (关联类型)

Swift中的Protocol不可以使用类型参数定义泛型,不过Protocol可以使用typealias关键字来定义一些关联类型:

// A protocol for things that can accept food.
protocol FoodEatingType {
  typealias Food

  var isSatiated : Bool { get }
  func feed(food: Food)
}

在这个实例中,Food是定义在FoodEatingType协议中的关联类型,而某个协议中的关联类型数目可以根据实际的需求数量定义。

关联类型,类似于类型参数,都是一种占位符。而后面如果某个类需要实现这个协议,则需要确定Food的具体类型,是Hay还是Rabbit。具体的做法就是继承并且实现协议的属性与方法,并在实现时再判断应该用哪些真实的数据类型去替换这些关联类型:

class Koala : FoodEatingType {
  var foodLevel = 0
  var isSatiated : Bool { return foodLevel < 10 }

  // Koalas are notoriously picky eaters
  func feed(food: Eucalyptus) {
    // ...
    if !isSatiated {
      foodLevel += 1
    }
  }
}

对于Koala这个具体的实现者,关联类型Food被定义为了Eucalyptus,换言之,也就是Koala.Food被定义为了Eucalyptus。如果某个类型实现了多个协议,那么对于每个协议中的关联类型也都必须实现。而如果某个继承的类型也是个泛型,也可以使用类型参数去帮助确定关联类型:

// Gourmand Wolf is a picky eater and will only eat his or her favorite food.
// Individual wolves may prefer different foods,though.
class GourmandWolf<FoodType> : FoodEatingType {
  var isSatiated : Bool { return false }

  func feed(food: FoodType) {
    // ...
  }
}

let meela = GourmandWolf<Rabbit>()
let rabbit = Rabbit()
meela.feed(rabbit)

在上述代码中,GourmandWolf<Goose>.FoodGoose,而 GourmandWolf<Sheep>.FoodSheep。顺便说一句,协议中的关联类型虽然属于泛型,但也可以为其添加一些约束或者父类。譬如我们定义的某个协议中为Heap添加了一些列的操作,并且要保证所有的Heap的Key是可比较的,即必须是Comparable的子类或者实现,那么可以添加如下约束:

// Types that conform represent simple max-heaps which use their elements as keys
protocol MaxHeapType {
  // Elements must support comparison ops; e.g. 'a is greater than b'.
  typealias Element : Comparable

  func insert(x: Element)
  func findMax() -> Element?
  func deleteMax() -> Bool
}

Type constraints (类型约束)

截至目前,我们提及的泛型,诸如T以及U可以用任意类型来替换。而标准库中的Array类型即是这样一种无任何约束的典型,可以由其类型参数来决断。譬如下面一个例子中,需要编写一个函数,输入一个数组而获取数组中最大的那个值并且返回:

// Doesn't compile.
func findLargestInArray<T>(array: Array<T>) -> T? {
  if array.isEmpty { return nil }
  var soFar : T = array[0]
  for i in 1..<array.count {
    soFar = array[i] > soFar ? array[i] : soFar
  }
  return soFar
}

不过这样毫无限制的泛型对于编译器是非常不友好的,如上述代码中需要进行一个比较,即array[i] > soFar,而我们只知道array[i]是类型T,并且soFar也是类型T。但是编译器根本不知道这个类型T能否进行比较。譬如我们创建一个空的结构体Foo,并且把它当做类型参数传了进来,那么我们压根不知道>这个比较运算符能否起作用。

在上文对于Protocol的讨论中,我们已经尝试使用:为某个关联类型设置一些约束,而在刚才的例子中,如果传入的String类型是可以正常编译的,但是一旦传入的是NSView类型,则不能正常编译了。而我们可以以如下方式添加约束:

// Note that <T> became <T : Comparable>,meaning that whatever type fills in
// 'T' must conform to Comparable.
func findLargestInArray<T : Comparable>(array: Array<T>) -> T? {
  if array.isEmpty { return nil }
  var soFar : T = array[0]
  for i in 1..<array.count {
    soFar = array[i] > soFar ? array[i] : soFar
  }
  return soFar
}

// Example usage:
let arrayToTest = [1,2,3,100,12]
// We're calling 'findLargestInArray()' with T = Int.
if let result = findLargestInArray(arrayToTest) {
  print("the largest element in the array is \(result)")
} else {
  print("the array was empty...")
}
// prints: "the largest element in the array is 100"

这是因为,在某种意义上,存在着一种悖论,越限制类型参数与添加约束,用户越能方便地使用这些参数。不加任何限制地类型参数往往只能使用在简单地交换或者从集合中添加或者删除某些元素。

简单约束

func example<T,U : Equatable,V : Hashable>(foo: T,bar: U,baz: V) {
  // ...
}

如果需要对泛型进行细粒度的控制,首先来讨论下在哪些地方可以进行控制处理:

  • 类似于U,V这样的类型参数,即上文中的普通的泛型参数

  • 类型参数的关联类型,即上文中协议里的关联类型。

Swift中提供了以下三个类型的约束:

  • T : SomeProtocol:类型T必须遵从协议SomeProtocol。需要注意,使用protocol<Foo,Bar>

  • T==U:类型参数T必须是类型参数或者关联类型U。

  • T:SomeClass:T必须是一个类,更加具体而言,T必须是SomeClass的一个实例或者它的子类。

Putting it all together

上文中已经介绍了基本的泛型的用法和约束,而对于泛型类型的签名可以综合使用如下:

  • 声明类型参数,如果愿意的话,最好每个类型参数都要声明遵循某个协议。

  • 使用where关键字。

  • 使用逗号分割声明约束。

protocol Foo {
  typealias Key
  typealias Element
}

protocol Bar {
  typealias RawGeneratorType
}

func example<T : Foo,U,V where V : Foo,V : Bar,T.Key == V.RawGeneratorType,U == V.Element>
  (arg1: T,arg2: U,arg3: V) -> U {
  // ...
}

不要惊慌,我们会一步一步地介绍这些泛型的用法。在where关键字之前,我们声明了三个类型参数:T,U以及V。其中T必须遵循Foo协议。而在where关键字之后,我们声明了四个约束:

  • V必须遵循Foo协议。

  • V也必须实现了Bar协议。

  • 由于T实现了Foo协议,T有一个关联类型为T.Key。V还有另一个关联类型,V.RawGeneratorType。这两个类型必须是相同的:T.Key == V.RawGeneratorType。

  • 因为V实现了协议Foo,V包含一个关联类型V.Element。这个类型必须与U一致,U == V.Element。

综上所述,无论何时使用example()函数时,必须要选择合适的T、U以及V类型。

Constrained extensions

Swift 2中新近提出了约束扩展的概念,一个更强大的能够使用泛型的语法特性。在Swift中,扩展允许向任意类型,即使尚未定义的类型中添加方法。同样允许向某个协议中添加默认的实现方法。同样的,这样的基于约束的扩展可以方便某些泛型的用法:

  • 对于像Array这样的泛型,可以在类型参数符合某个特定的约束的情况下添加某个方法。

  • 对于像CollectionType这样包含关联类型的协议,可以当某个关联类型符合某个约束时添加默认的实现方法。

Syntax and limitations (语法与限制)

基于约束的扩展语法如下所示:

// Methods in this extension are only available to Arrays whose elements are
// both hashable and comparable.
extension Array where Element : Hashable,Element : Comparable {
  // ...
}

where关键字跟随在类型参数之后,而后跟着以逗号分割的一系列泛型类型和参数。不过这其中的约束中并不能限定为非泛型,即:

// An extension on Array<Int>.
// Error: "Same-type requirement makes generic parameter 'Element' non-generic"
extension Array where Element == Int {
  // ...
}

同时,也不能进行协议的传递:

protocol MyProtocol { /* ... */ }

// Only Arrays with comparable elements conform to MyProtocol.
// Error: "Extension of type 'Array' with constraints cannot have an inheritance clause"
extension Array : MyProtocol where Element : Comparable {
  // ...
}

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


软件简介:蓝湖辅助工具,减少移动端开发中控件属性的复制和粘贴.待开发的功能:1.支持自动生成约束2.开发设置页面3.做一个浏览器插件,支持不需要下载整个工程,可即时操作当前蓝湖浏览页面4.支持Flutter语言模板生成5.支持更多平台,如Sketch等6.支持用户自定义语言模板
现实生活中,我们听到的声音都是时间连续的,我们称为这种信号叫模拟信号。模拟信号需要进行数字化以后才能在计算机中使用。目前我们在计算机上进行音频播放都需要依赖于音频文件。那么音频文件如何生成的呢?音频文件的生成过程是将声音信息采样、量化和编码产生的数字信号的过程,我们人耳所能听到的声音频率范围为(20Hz~20KHz),因此音频文件格式的最大带宽是20KHZ。根据奈奎斯特的理论,音频文件的采样率一般在40~50KHZ之间。奈奎斯特采样定律,又称香农采样定律。...............
前言最近在B站上看到一个漂亮的仙女姐姐跳舞视频,循环看了亿遍又亿遍,久久不能离开!看着小仙紫姐姐的蹦迪视频,除了一键三连还能做什么?突发奇想,能不能把舞蹈视频转成代码舞呢?说干就干,今天就手把手教大家如何把跳舞视频转成代码舞,跟着仙女姐姐一起蹦起来~视频来源:【紫颜】见过仙女蹦迪吗 【千盏】一、核心功能设计总体来说,我们需要分为以下几步完成:从B站上把小姐姐的视频下载下来对视频进行截取GIF,把截取的GIF通过ASCII Animator进行ASCII字符转换把转换的字符gif根据每
【Android App】实战项目之仿抖音的短视频分享App(附源码和演示视频 超详细必看)
前言这一篇博客应该是我花时间最多的一次了,从2022年1月底至2022年4月底。我已经将这篇博客的内容写为论文,上传至arxiv:https://arxiv.org/pdf/2204.10160.pdf欢迎大家指出我论文中的问题,特别是语法与用词问题在github上,我也上传了完整的项目:https://github.com/Whiffe/Custom-ava-dataset_Custom-Spatio-Temporally-Action-Video-Dataset关于自定义ava数据集,也是后台
因为我既对接过session、cookie,也对接过JWT,今年因为工作需要也对接了gtoken的2个版本,对这方面的理解还算深入。尤其是看到官方文档评论区又小伙伴表示看不懂,所以做了这期视频内容出来:视频在这里:本期内容对应B站的开源视频因为涉及的知识点比较多,视频内容比较长。如果你觉得看视频浪费时间,可以直接阅读源码:goframe v2版本集成gtokengoframe v1版本集成gtokengoframe v2版本集成jwtgoframe v2版本session登录官方调用示例文档jwt和sess
【Android App】实战项目之仿微信的私信和群聊App(附源码和演示视频 超详细必看)
用Android Studio的VideoView组件实现简单的本地视频播放器。本文将讲解如何使用Android视频播放器VideoView组件来播放本地视频和网络视频,实现起来还是比较简单的。VideoView组件的作用与ImageView类似,只是ImageView用于显示图片,VideoView用于播放视频。...
采用MATLAB对正弦信号,语音信号进行生成、采样和内插恢复,利用MATLAB工具箱对混杂噪声的音频信号进行滤波
随着移动互联网、云端存储等技术的快速发展,包含丰富信息的音频数据呈现几何级速率增长。这些海量数据在为人工分析带来困难的同时,也为音频认知、创新学习研究提供了数据基础。在本节中,我们通过构建生成模型来生成音频序列文件,从而进一步加深对序列数据处理问题的了解。
基于yolov5+deepsort+slowfast算法的视频实时行为检测。1. yolov5实现目标检测,确定目标坐标 2. deepsort实现目标跟踪,持续标注目标坐标 3. slowfast实现动作识别,并给出置信率 4. 用框持续框住目标,并将动作类别以及置信度显示在框上
数字电子钟设计本文主要完成数字电子钟的以下功能1、计时功能(24小时)2、秒表功能(一个按键实现开始暂停,另一个按键实现清零功能)3、闹钟功能(设置闹钟以及到时响10秒)4、校时功能5、其他功能(清零、加速、星期、八位数码管显示等)前排提示:前面几篇文章介绍过的内容就不详细介绍了,可以看我专栏的前几篇文章。PS.工程文件放在最后面总体设计本次设计主要是在前一篇文章 数字电子钟基本功能的实现 的基础上改编而成的,主要结构不变,分频器将50MHz分为较低的频率备用;dig_select
1.进入官网下载OBS stdioOpen Broadcaster Software | OBS (obsproject.com)2.下载一个插件,拓展OBS的虚拟摄像头功能链接:OBS 虚拟摄像头插件.zip_免费高速下载|百度网盘-分享无限制 (baidu.com)提取码:6656--来自百度网盘超级会员V1的分享**注意**该插件必须下载但OBS的根目录(应该是自动匹配了的)3.打开OBS,选中虚拟摄像头选择启用在底部添加一段视频录制选择下面,进行录制.
Meta公司在9月29日首次推出一款人工智能系统模型:Make-A-Video,可以从给定的文字提示生成短视频。基于**文本到图像生成技术的最新进展**,该技术旨在实现文本到视频的生成,可以仅用几个单词或几行文本生成异想天开、独一无二的视频,将无限的想象力带入生活
音频信号叠加噪声及滤波一、前言二、信号分析及加噪三、滤波去噪四、总结一、前言之前一直对硬件上的内容比较关注,但是可能是因为硬件方面的东西可能真的是比较杂,而且需要渗透的东西太多了,所以学习进展比较缓慢。因为也很少有单纯的硬件学习研究,总是会伴随着各种理论需要硬件做支撑,所以还是想要慢慢接触理论学习。但是之前总找不到切入点,不知道从哪里开始,就一直拖着。最近稍微接触了一点信号处理,就用这个当作切入点,开始接触理论学习。二、信号分析及加噪信号处理选用了matlab做工具,选了一个最简单的语音信号处理方
腾讯云 TRTC 实时音视频服务体验,从认识 TRTC 到 TRTC 的开发实践,Demo 演示& IM 服务搭建。
音乐音频分类技术能够基于音乐内容为音乐添加类别标签,在音乐资源的高效组织、检索和推荐等相关方面的研究和应用具有重要意义。传统的音乐分类方法大量使用了人工设计的声学特征,特征的设计需要音乐领域的知识,不同分类任务的特征往往并不通用。深度学习的出现给更好地解决音乐分类问题提供了新的思路,本文对基于深度学习的音乐音频分类方法进行了研究。首先将音乐的音频信号转换成声谱作为统一表示,避免了手工选取特征存在的问题,然后基于一维卷积构建了一种音乐分类模型。
C++知识精讲16 | 井字棋游戏(配资源+视频)【赋源码,双人对战】
本文主要讲解如何在Java中,使用FFmpeg进行视频的帧读取,并最终合并成Gif动态图。
在本篇博文中,我们谈及了 Swift 中 some、any 关键字以及主关联类型(primary associated types)的前世今生,并由浅及深用简明的示例向大家讲解了它们之间的奥秘玄机。