LoginSignup
5
3

More than 3 years have passed since last update.

[Swift] classでもselfに代入したい!

Posted at

目標

init(_ otherInstance: Self) {
  self = otherInstance
}

というようなことをclassでもしてみたいけど、どうしたらできる…?
(想定: Swift 5現在)

はじめに: selfへの代入ができるのはどんなとき?

構造体(struct)

次のように、イニシャライザやmutating funcの中でselfに代入をすることができます:

struct S {
  init() {}

  init(_ otherInstance: S) {
    self = otherInstance
  }

  mutating func replace(with otherInstance: S) {
    self = otherInstance
  }
}

let s1 = S()
let s2 = S(s1)
var s3 = S()
s3.replace(with: s2)

列挙型(enum)

列挙型も構造体と同様です:

enum E {
  case a, b

  init() {
    self = .a
  }

  init(_ otherInstance: E) {
    self = otherInstance
  }

  mutating func replace(with otherInstance: E) {
    self = otherInstance
  }
}

let e1 = E() // .a
let e2 = E.b
var e3 = E(e2) // .b
e3.replace(with: e1) // .a

クラス(class)では…?

クラスで同様のコードを書いてみると…

class C {
  init() {}

  init(_ otherInstance: C) {
    self = otherInstance // ⛔️ error: cannot assign to value: 'self' is immutable
  }

  func replace(with otherInstance: C) {
    self = otherInstance // ⛔️ error: cannot assign to value: 'self' is immutable
  }
}

selfがimmutableだから代入できないと怒られてしまいました…。
Swiftのclassにおけるselfは(structenumのそれと違って)、実体としてのインスタンスを指し示すものではなく、実体を参照する(C言語的にいえば)ポインタを指し示すものなので、self自体は常にimmutableなのです1

どうしてもclassでもselfに代入したい

 前述のように単純なコードでは無理ですが、実は、コードをこねくりまわせばclassにおいてもselfに代入ができるのです。…正確にはselfに代入したかのような動作をするコードを書くことができるのです。
 どうするのかというと、前項には登場しなかったアレ…そう、プロトコルを使うのです。
 まずは、プロトコルのコードをみてみましょう:

protocol P {}
extension P {
  init(otherInstance: Self) {
    self = otherInstance
  }
}

このように、プロトコルにおいてもselfへの代入が許容されているのです。
あとは、クラスをこのプロトコルに準拠させて、このプロトコルで定義されているイニシャライザを呼び出せば…

class CP: P {
  convenience init(_ otherInstance: CP) {
    self.init(otherInstance: otherInstance as! Self)
  }
}

できました!

注意点

上記のコードではas! Selfという強制型変換が必要になります。要するにCPのサブクラスでこのイニシャライザを呼んだ時の安全性が確保されません。なので、CPfinal classとして宣言したり、プロトコルPprivateで宣言したりすることが望まれます。

使用例

1. クラスクラスタ

ちょうどいい例がGistにありました:
https://gist.github.com/JadenGeller/1f4acd5cd07d4642c246d87ca25bd3e0

2. 同一入力に単一インスタンス

// 生成されたインスタンスを格納
private var _instances: [Int: MyClass] = [:] 

// selfへの代入を可能にするためのプロトコル
private protocol _MyClass {}
extension _MyClass where Self: MyClass {
  fileprivate init(_integer integer: Int) {
    if let instance = _instances[integer] {
      self = instance as! Self
    } else {
      self.init(__integer: integer)
      _instances[integer] = self
    }
  }
}

// 今回メインのクラス
// "designated initializer"は外部から呼べないようにし、
// "convenience initializer"の内部でプロトコルのイニシャライザを呼び出す
public final class MyClass: _MyClass {
  public let integer: Int

  fileprivate init(__integer integer: Int) {
    self.integer = integer
  }

  public convenience init(_ integer: Int) {
    self.init(_integer: integer)
  }
}

print(ObjectIdentifier(MyClass(0)) == ObjectIdentifier(MyClass(0))) // -> true
print(ObjectIdentifier(MyClass(0)) == ObjectIdentifier(MyClass(1))) // -> false
print(ObjectIdentifier(MyClass(1)) == ObjectIdentifier(MyClass(1))) // -> true

おわりに

いかがだったでしょうか。
classでもselfに代入したい場面というのはそこまで多くはないですが、ゼロではないはずです。方法が存在するということを知っておくだけでも、今後どこかで役に立つかもしれません。…というか役に立つといいなと思っています。


  1. 逆にプロパティの書き換えは、インスタンスを参照するポインタの値が書き変わるわけではないので、メソッドの宣言などでmutatingというキーワードが不要ということになります。 

5
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3