TensorFlow.org で見る | Google Colab で実行する | GitHub でソースを表示 |
このチュートリアルでは、プロトコル指向のプログラミングと、それらを日常的な例でジェネリックで使用する方法のさまざまな例について説明します。
プロトコル
継承はプログラミング言語のコードを編成する強力な方法であり、プログラムの複数のコンポーネント間でコードを共有できるようになります。
Swift では、継承を表現するさまざまな方法があります。他の言語での方法の 1 つであるクラス継承については、すでにご存知かもしれません。ただし、Swift にはプロトコルという別の方法があります。
このチュートリアルでは、さまざまなトレードオフを通じて同様の目標を達成できるサブクラス化の代替手段であるプロトコルについて説明します。 Swift では、プロトコルには複数の抽象メンバーが含まれます。クラス、構造体、および列挙型は複数のプロトコルに準拠でき、準拠関係は遡って確立できます。これらすべてにより、サブクラス化を使用して Swift で簡単に表現できない一部の設計が可能になります。プロトコルの使用 (拡張機能とプロトコル制約) をサポートするイディオムと、プロトコルの制限について説明します。
Swift 💖 の値のタイプ!
参照セマンティクスを持つクラスに加えて、Swift は値によって渡される列挙型と構造体をサポートします。列挙型と構造体は、クラスによって提供される多くの機能をサポートします。見てみましょう!
まず、列挙型がクラスとどのように似ているかを見てみましょう。
enum Color: String {
case red = "red"
case green = "green"
case blue = "blue"
// A computed property. Note that enums cannot contain stored properties.
var hint: String {
switch self {
case .red:
return "Roses are this color."
case .green:
return "Grass is this color."
case .blue:
return "The ocean is this color."
}
}
// An initializer like for classes.
init?(color: String) {
switch color {
case "red":
self = .red
case "green":
self = .green
case "blue":
self = .blue
default:
return nil
}
}
}
// Can extend the enum as well!
extension Color {
// A function.
func hintFunc() -> String {
return self.hint
}
}
let c = Color.red
print("Give me a hint for c: \(c.hintFunc())")
let invalidColor = Color(color: "orange")
print("is invalidColor nil: \(invalidColor == nil)")
Give me a hint for c: Roses are this color. is invalidColor nil: true
次に、構造体を見てみましょう。構造体を継承することはできませんが、代わりにプロトコルを使用できることに注意してください。
struct FastCar {
// Can have variables and constants as stored properties.
var color: Color
let horsePower: Int
// Can have computed properties.
var watts: Float {
return Float(horsePower) * 745.7
}
// Can have lazy variables like in classes!
lazy var titleCaseColorString: String = {
let colorString = color.rawValue
return colorString.prefix(1).uppercased() +
colorString.lowercased().dropFirst()
}()
// A function.
func description() -> String {
return "This is a \(color) car with \(horsePower) horse power!"
}
// Can create a variety of initializers.
init(color: Color, horsePower: Int) {
self.color = color
self.horsePower = horsePower
}
// Can define extra initializers other than the default one.
init?(color: String, horsePower: Int) {
guard let enumColor = Color(color: color) else {
return nil
}
self.color = enumColor
self.horsePower = horsePower
}
}
var car = FastCar(color: .red, horsePower: 250)
print(car.description())
print("Horse power in watts: \(car.watts)")
print(car.titleCaseColorString)
This is a red car with 250 horse power! Horse power in watts: 186425.0 Red
最後に、クラスとは異なり、値型によってどのように渡されるかを見てみましょう。
// Notice we have no problem modifying a constant class with
// variable properties.
class A {
var a = "a"
}
func foo(_ a: A) {
a.a = "foo"
}
let a = A()
print(a.a)
foo(a)
print(a.a)
/*
Uncomment the following code to see how an error is thrown.
Structs are implicitly passed by value, so we cannot modify it.
> "error: cannot assign to property: 'car' is a 'let' constant"
*/
// func modify(car: FastCar, toColor color: Color) -> Void {
// car.color = color
// }
// car = FastCar(color: .red, horsePower: 250)
// print(car.description())
// modify(car: &car, toColor: .blue)
// print(car.description())
a foo
プロトコルを使ってみよう
さまざまな車のプロトコルを作成することから始めましょう。
protocol Car {
var color: Color { get set }
var price: Int { get }
func turnOn()
mutating func drive()
}
protocol Electric {
mutating func recharge()
// percentage of the battery level, 0-100%.
var batteryLevel: Int { get set }
}
protocol Gas {
mutating func refill()
// # of liters the car is holding, varies b/w models.
var gasLevelLiters: Int { get set }
}
オブジェクト指向の世界 (多重継承がない) では、 Electric
とGas
の抽象クラスを作成し、クラス継承を使用して両方をCar
から継承し、特定の自動車モデルを基本クラスにすることができます。ただし、ここでは両方が結合ゼロの完全に別個のプロトコルです。これにより、システム全体の設計方法がより柔軟になります。
テスラを定義しましょう。
struct TeslaModelS: Car, Electric {
var color: Color // Needs to be a var since `Car` has a getter and setter.
let price: Int
var batteryLevel: Int
func turnOn() {
print("Starting all systems!")
}
mutating func drive() {
print("Self driving engaged!")
batteryLevel -= 8
}
mutating func recharge() {
print("Recharging the battery...")
batteryLevel = 100
}
}
var tesla = TeslaModelS(color: .red, price: 110000, batteryLevel: 100)
これは、 Car
とElectric
の両方のプロトコルに準拠する新しい構造体TeslaModelS
を指定します。
次に、ガソリン車を定義しましょう。
struct Mustang: Car, Gas{
var color: Color
let price: Int
var gasLevelLiters: Int
func turnOn() {
print("Starting all systems!")
}
mutating func drive() {
print("Time to drive!")
gasLevelLiters -= 1
}
mutating func refill() {
print("Filling the tank...")
gasLevelLiters = 25
}
}
var mustang = Mustang(color: .red, price: 30000, gasLevelLiters: 25)
デフォルトの動作でプロトコルを拡張する
例からわかることは、ある程度の冗長性があるということです。電気自動車を充電するたびに、バッテリーのパーセンテージ レベルを 100 に設定する必要があります。すべての電気自動車の最大容量は 100% ですが、ガソリン車はガソリン タンクの容量によって異なるため、電気自動車の場合はデフォルトのレベルを 100 に設定できます。 。
ここで Swift の拡張機能が役に立ちます。
extension Electric {
mutating func recharge() {
print("Recharging the battery...")
batteryLevel = 100
}
}
したがって、私たちが作成する新しい電気自動車は、充電時にバッテリーを 100 に設定します。したがって、クラス、構造体、および列挙型を独自のデフォルトの動作で修飾することができました。
コミックを提供してくれたレイ・ヴェンダーリッヒに感謝します!
ただし、次の点に注意してください。最初の実装では、 foo()
A
のデフォルト実装として定義しますが、プロトコルでは必須にしません。したがって、 a.foo()
を呼び出すと、「 A default
」が出力されます。
protocol Default {}
extension Default {
func foo() { print("A default")}
}
struct DefaultStruct: Default {
func foo() {
print("Inst")
}
}
let a: Default = DefaultStruct()
a.foo()
A default
ただし、 A
でfoo()
必須にすると、「 Inst
」が返されます。
protocol Default {
func foo()
}
extension Default {
func foo() {
print("A default")
}
}
struct DefaultStruct: Default {
func foo() {
print("Inst")
}
}
let a: Default = DefaultStruct()
a.foo()
Inst
これは、Swift のプロトコルにおける最初の例の静的ディスパッチと 2 番目の例の静的ディスパッチの違いにより発生します。詳細については、このMedium 投稿を参照してください。
デフォルトの動作をオーバーライドする
ただし、必要に応じて、デフォルトの動作をオーバーライドすることもできます。注意すべき重要な点の 1 つは、これは動的ディスパッチをサポートしていないことです。
古いバージョンの電気自動車があるため、バッテリーの状態が 90% まで低下しているとします。
struct OldElectric: Car, Electric {
var color: Color // Needs to be a var since `Car` has a getter and setter.
let price: Int
var batteryLevel: Int
func turnOn() {
print("Starting all systems!")
}
mutating func drive() {
print("Self driving engaged!")
batteryLevel -= 8
}
mutating func reCharge() {
print("Recharging the battery...")
batteryLevel = 90
}
}
標準ライブラリでのプロトコルの使用
Swift のプロトコルがどのように機能するか理解できたので、標準ライブラリ プロトコルの典型的な使用例をいくつか見てみましょう。
標準ライブラリを拡張する
Swift にすでに存在する型に機能を追加する方法を見てみましょう。 Swift の型は組み込まれているのではなく、構造体として標準ライブラリの一部であるため、これを行うのは簡単です。
配列がソートされていることを確認しながら、要素の配列に対して二分検索を実行してみましょう。
extension Collection where Element: Comparable {
// Verify that a `Collection` is sorted.
func isSorted(_ order: (Element, Element) -> Bool) -> Bool {
var i = index(startIndex, offsetBy: 1)
while i < endIndex {
// The longer way of calling a binary function like `<(_:_:)`,
// `<=(_:_:)`, `==(_:_:)`, etc.
guard order(self[index(i, offsetBy: -1)], self[i]) else {
return false
}
i = index(after: i)
}
return true
}
// Perform binary search on a `Collection`, verifying it is sorted.
func binarySearch(_ element: Element) -> Index? {
guard self.isSorted(<=) else {
return nil
}
var low = startIndex
var high = endIndex
while low <= high {
let mid = index(low, offsetBy: distance(from: low, to: high)/2)
if self[mid] == element {
return mid
} else if self[mid] < element {
low = index(after: mid)
} else {
high = index(mid, offsetBy: -1)
}
}
return nil
}
}
print([2, 2, 5, 7, 11, 13, 17].binarySearch(5)!)
print(["a", "b", "c", "d"].binarySearch("b")!)
print([1.1, 2.2, 3.3, 4.4, 5.5].binarySearch(3.3)!)
2 1 2
これは、「要素が非破壊的に複数回横断され、インデックス付き添字によってアクセスできるシーケンス」を定義するCollection
プロトコルを拡張することで実現します。配列には角括弧表記を使用してインデックスを付けることができるため、これが拡張したいプロトコルです。
同様に、このユーティリティ関数は、要素を比較できる配列にのみ追加したいと考えています。これが、 where Element: Comparable
ある理由です。
where
句は Swift の型システムの一部であり、これについてはすぐに説明しますが、要するに、プロトコルを実装する型を要求する、プロトコルを実装するために 2 つの型を要求するなど、作成中の拡張機能に追加の要件を追加します。または、クラスに特定のスーパークラスを持つことを要求します。
Element
は、 Collection
準拠型の要素に関連付けられた型です。 Element
はSequence
プロトコル内で定義されますが、 Collection
Sequence
を継承するため、 Element
関連型を継承します。
Comparable
は、「関係演算子<
、 <=
、 >=
、および>
を使用して比較できる型」を定義するプロトコルです。 。ソートされたCollection
に対してバイナリ検索を実行しているため、これはもちろん true でなければなりません。そうしないと、バイナリ検索で左または右のどちらに再帰/反復するかがわかりません。
実装に関する補足として、使用されたindex(_:offsetBy:)
関数の詳細については、次のドキュメントを参照してください。
ジェネリック + プロトコル = 💥
ジェネリックとプロトコルは、コードの重複を避けるために正しく使用すれば、強力なツールになります。
まず、別のチュートリアルであるA Swift Tourに目を通してください。このチュートリアルでは、Colab 本の最後にジェネリックについて簡単に説明しています。
ジェネリックについて一般的な知識があると仮定して、いくつかの高度な使用法を簡単に見てみましょう。
複数のプロトコルに準拠するタイプなど、1 つのタイプに複数の要件がある場合、自由に使用できるオプションがいくつかあります。
typealias ComparableReal = Comparable & FloatingPoint
func foo1<T: ComparableReal>(a: T, b: T) -> Bool {
return a > b
}
func foo2<T: Comparable & FloatingPoint>(a: T, b: T) -> Bool {
return a > b
}
func foo3<T>(a: T, b: T) -> Bool where T: ComparableReal {
return a > b
}
func foo4<T>(a: T, b: T) -> Bool where T: Comparable & FloatingPoint {
return a > b
}
func foo5<T: FloatingPoint>(a: T, b: T) -> Bool where T: Comparable {
return a > b
}
print(foo1(a: 1, b: 2))
print(foo2(a: 1, b: 2))
print(foo3(a: 1, b: 2))
print(foo4(a: 1, b: 2))
print(foo5(a: 1, b: 2))
false false false false false
上部でtypealias
が使用されていることに注目してください。これにより、既存のタイプの名前付きエイリアスがプログラムに追加されます。型の別名を宣言すると、プログラム内のどこでも既存の型の代わりにその別名を使用できます。型エイリアスは新しい型を作成しません。単に名前が既存の型を参照できるようにするだけです。
ここで、プロトコルとジェネリックをどのように併用できるかを見てみましょう。
私たちがコンピューター ストアであると想像してください。店舗の奥でラップトップをどのように整理するかを決定するために、販売するラップトップに次の要件があるとします。
enum Box {
case small
case medium
case large
}
enum Mass {
case light
case medium
case heavy
}
// Note: `CustomStringConvertible` protocol lets us pretty-print a `Laptop`.
struct Laptop: CustomStringConvertible {
var name: String
var box: Box
var mass: Mass
var description: String {
return "(\(self.name) \(self.box) \(self.mass))"
}
}
ただし、棚には重量制限があるため、 Laptop
を質量別にグループ化するという新しい要件があります。
func filtering(_ laptops: [Laptop], by mass: Mass) -> [Laptop] {
return laptops.filter { $0.mass == mass }
}
let laptops: [Laptop] = [
Laptop(name: "a", box: .small, mass: .light),
Laptop(name: "b", box: .large, mass: .medium),
Laptop(name: "c", box: .medium, mass: .heavy),
Laptop(name: "d", box: .large, mass: .light)
]
let filteredLaptops = filtering(laptops, by: .light)
print(filteredLaptops)
[(a small light), (d large light)]
しかし、 Mass
以外のものでフィルタリングしたい場合はどうすればよいでしょうか?
1 つのオプションは、次のことを行うことです。
// Define a protocol which will act as our comparator.
protocol DeviceFilterPredicate {
associatedtype Device
func shouldKeep(_ item: Device) -> Bool
}
// Define the structs we will use for passing into our filtering function.
struct BoxFilter: DeviceFilterPredicate {
typealias Device = Laptop
var box: Box
func shouldKeep(_ item: Laptop) -> Bool {
return item.box == box
}
}
struct MassFilter: DeviceFilterPredicate {
typealias Device = Laptop
var mass: Mass
func shouldKeep(_ item: Laptop) -> Bool {
return item.mass == mass
}
}
// Make sure our filter conforms to `DeviceFilterPredicate` and that we are
// filtering `Laptop`s.
func filtering<F: DeviceFilterPredicate>(
_ laptops: [Laptop],
by filter: F
) -> [Laptop] where Laptop == F.Device {
return laptops.filter { filter.shouldKeep($0) }
}
// Let's test the function out!
print(filtering(laptops, by: BoxFilter(box: .large)))
print(filtering(laptops, by: MassFilter(mass: .heavy)))
[(b large medium), (d large light)] [(c medium heavy)]
素晴らしい!これで、ラップトップの制約に基づいてフィルタリングできるようになりました。ただし、フィルターできるのはLaptop
のみです。
箱に入っていて質量のあるものであれば何でもフィルタリングできるのはどうでしょうか?おそらく、このラップトップの倉庫は、別の顧客ベースを持つサーバーにも使用されるでしょう。
// Define 2 new protocols so we can filter anything in a box and which has mass.
protocol Weighable {
var mass: Mass { get }
}
protocol Boxed {
var box: Box { get }
}
// Define the new Laptop and Server struct which have mass and a box.
struct Laptop: CustomStringConvertible, Boxed, Weighable {
var name: String
var box: Box
var mass: Mass
var description: String {
return "(\(self.name) \(self.box) \(self.mass))"
}
}
struct Server: CustomStringConvertible, Boxed, Weighable {
var isWorking: Bool
var name: String
let box: Box
let mass: Mass
var description: String {
if isWorking {
return "(working \(self.name) \(self.box) \(self.mass))"
} else {
return "(notWorking \(self.name) \(self.box) \(self.mass))"
}
}
}
// Define the structs we will use for passing into our filtering function.
struct BoxFilter<T: Boxed>: DeviceFilterPredicate {
var box: Box
func shouldKeep(_ item: T) -> Bool {
return item.box == box
}
}
struct MassFilter<T: Weighable>: DeviceFilterPredicate {
var mass: Mass
func shouldKeep(_ item: T) -> Bool {
return item.mass == mass
}
}
// Define the new filter function.
func filtering<F: DeviceFilterPredicate, T>(
_ elements: [T],
by filter: F
) -> [T] where T == F.Device {
return elements.filter { filter.shouldKeep($0) }
}
// Let's test the function out!
let servers = [
Server(isWorking: true, name: "serverA", box: .small, mass: .heavy),
Server(isWorking: false, name: "serverB", box: .medium, mass: .medium),
Server(isWorking: true, name: "serverC", box: .large, mass: .light),
Server(isWorking: false, name: "serverD", box: .medium, mass: .light),
Server(isWorking: true, name: "serverE", box: .small, mass: .heavy)
]
let products = [
Laptop(name: "a", box: .small, mass: .light),
Laptop(name: "b", box: .large, mass: .medium),
Laptop(name: "c", box: .medium, mass: .heavy),
Laptop(name: "d", box: .large, mass: .light)
]
print(filtering(servers, by: BoxFilter(box: .small)))
print(filtering(servers, by: MassFilter(mass: .medium)))
print(filtering(products, by: BoxFilter(box: .small)))
print(filtering(products, by: MassFilter(mass: .medium)))
[(working serverA small heavy), (working serverE small heavy)] [(notWorking serverB medium medium)] [(a small light)] [(b large medium)]
特定のstruct
のプロパティだけでなく、そのプロパティを持つ任意の struct もフィルタできるようになりました。
優れた API 設計のためのヒント
このセクションは、 WWDC 2019: Modern Swift API Designの講演から抜粋したものです。
プロトコルがどのように動作するかを理解したので、次にプロトコルを使用する必要がある場合について検討するのが最善です。プロトコルは非常に強力ですが、すぐにプロトコルを開始することが必ずしも最善のアイデアであるとは限りません。
- 具体的な使用例から始めます。
- まず、具体的なタイプを使用してユースケースを調査し、共有したいコードと繰り返されているコードを理解します。次に、その共有コードをジェネリックスと因数分解します。それは新しいプロトコルを作成することを意味するかもしれません。汎用コードの必要性を発見します。
- 標準ライブラリで定義されている既存のプロトコルから新しいプロトコルを作成することを検討してください。この良い例については、次のApple ドキュメントを参照してください。
- ジェネリック プロトコルの代わりにジェネリック タイプを定義することを検討してください。
例: カスタム ベクトル タイプの定義
作成中のジオメトリ アプリで使用する浮動小数点数に関するGeometricVector
プロトコルを定義するとします。これは 3 つの重要なベクトル演算を定義します。
protocol GeometricVector {
associatedtype Scalar: FloatingPoint
static func dot(_ a: Self, _ b: Self) -> Scalar
var length: Scalar { get }
func distance(to other: Self) -> Scalar
}
SIMD
プロトコルが役立つベクトルの次元を保存したいとします。そのため、新しい型でSIMD
プロトコルを改良します。 SIMD
ベクトルは、ベクトル演算の実行に使用すると非常に高速な固定サイズのベクトルと考えることができます。
protocol GeometricVector: SIMD {
associatedtype Scalar: FloatingPoint
static func dot(_ a: Self, _ b: Self) -> Scalar
var length: Scalar { get }
func distance(to other: Self) -> Scalar
}
ここで、上記の操作のデフォルトの実装を定義しましょう。
extension GeometricVector {
static func dot(_ a: Self, _ b: Self) -> Scalar {
(a * b).sum()
}
var length: Scalar {
Self.dot(self, self).squareRoot()
}
func distance(to other: Self) -> Scalar {
(self - other).length
}
}
次に、これらの機能を追加する各タイプに適合性を追加する必要があります。
extension SIMD2: GeometricVector where Scalar: FloatingPoint { }
extension SIMD3: GeometricVector where Scalar: FloatingPoint { }
extension SIMD4: GeometricVector where Scalar: FloatingPoint { }
extension SIMD8: GeometricVector where Scalar: FloatingPoint { }
extension SIMD16: GeometricVector where Scalar: FloatingPoint { }
extension SIMD32: GeometricVector where Scalar: FloatingPoint { }
extension SIMD64: GeometricVector where Scalar: FloatingPoint { }
プロトコルを定義し、デフォルトの実装を与え、複数のタイプに適合性を追加するこの 3 段階のプロセスは、かなり反復的です。
プロトコルは必要でしたか?
どのSIMD
タイプにも独自の実装がないという事実は、危険信号です。したがって、この場合、プロトコルは実際には何も提供しません。
SIMD
の拡張で定義する
SIMD
プロトコルの拡張で 3 つの演算子を作成すると、問題をより簡潔に解決できます。
extension SIMD where Scalar: FloatingPoint {
static func dot(_ a: Self, _ b: Self) -> Scalar {
(a * b).sum()
}
var length: Scalar {
Self.dot(self, self).squareRoot()
}
func distance(to other: Self) -> Scalar {
(self - other).length
}
}
より少ないコード行を使用して、すべてのデフォルト実装をすべてのタイプのSIMD
に追加しました。
場合によっては、この型の階層を作成したくなるかもしれませんが、常に必要なわけではないことに注意してください。これは、コンパイルされたプログラムのバイナリ サイズが小さくなり、コードのコンパイルが速くなることも意味します。
ただし、この拡張アプローチは、追加するメソッドの数が少ない場合に最適です。ただし、より大規模な API を設計する場合には、スケーラビリティの問題が発生します。
ですか?があります?
先ほど、 GeometricVector
SIMD
を改良すると言いました。しかし、これはis-a関係なのでしょうか?問題は、 SIMD
ベクトルにスカラー 1 を追加できる演算を定義していることですが、ジオメトリのコンテキストでそのような演算を定義するのは意味がありません。
したがって、任意の浮動小数点数を処理できる新しいジェネリック型でSIMD
をラップすることで、has-a 関係がより良くなるかもしれません。
// NOTE: `Storage` is the underlying type that is storing the values,
// just like in a `SIMD` vector.
struct GeometricVector<Storage: SIMD> where Storage.Scalar: FloatingPoint {
typealias Scalar = Storage.Scalar
var value: Storage
init(_ value: Storage) { self.value = value }
}
次に、注意して、ジオメトリのコンテキストでのみ意味のある操作のみを定義することができます。
extension GeometricVector {
static func + (a: Self, b: Self) -> Self {
Self(a.value + b.value)
}
static func - (a: Self, b: Self) -> Self {
Self(a.value - b.value)
}
static func * (a: Self, b: Scalar) -> Self {
Self(a.value * b)
}
}
また、汎用拡張機能を引き続き使用して、以前とほぼ同じに見える、実装する必要があった 3 つの以前の演算子を取得できます。
extension GeometricVector {
static func dot(_ a: Self, _ b: Self) -> Scalar {
(a.value * b.value).sum()
}
var length: Scalar {
Self.dot(self, self).squareRoot()
}
func distance(to other: Self) -> Scalar {
(self - other).length
}
}
全体として、構造体を使用するだけで、3 つの操作の動作を型に合わせて調整することができました。プロトコルに関しては、すべてのSIMD
ベクトルに対して反復的な適合性を記述するという問題に直面しました。また、 Scalar + Vector
などの特定の演算子を使用できないようにすることもできませんでした (この場合、これは望ましくありませんでした)。したがって、プロトコルは万能の解決策ではないことを覚えておいてください。しかし、場合によっては、より伝統的なソリューションの方が強力であることが判明することがあります。
より多くのプロトコル指向のプログラミング リソース
ここで説明するトピックに関する追加リソースは次のとおりです。
- WWDC 2015: Swift でのプロトコル指向プログラミング: これは Swift 2 を使用して発表されたため、それ以来多くのことが変更されました (プレゼンテーションで使用されたプロトコルの名前など) が、これは依然として理論とその背後にある用途についての優れたリソースです。 。
- Swift 3 でのプロトコル指向プログラミングの紹介: これは Swift 3 で書かれているため、正常にコンパイルするにはコードの一部を変更する必要があるかもしれませんが、これも優れたリソースです。
- WWDC 2019: 最新の Swift API 設計: 値型と参照型の違い、API 設計においてプロトコルが最悪の選択であることが判明する場合のユースケース (上記の「優れた API 設計のためのヒント」セクションと同じ)、キーについて説明します。パス メンバーの検索とプロパティ ラッパー。
- ジェネリックス: ジェネリックスに関する Swift 5 用の Swift 独自のドキュメント。