0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

`let`と`var`の違い、値型と参照型の違いを理解する

Last updated at Posted at 2025-04-01

var にしてるのにプロパティ変更できない...
@propertyWrapperdidSet の使い所がイマイチわからない...

この記事では、

  • let / var と 値型 / 参照型 の関係性
  • 計算プロパティ・プロパティオブザーバ・プロパティラッパーの挙動と応用

について実例コードとともにまとめていきます。
私は初心者なので、間違いあれば指摘していただけると助かります。

letvar,そして値型と参照型

struct,値型のインスタンスを変更してみる

struct StructSample{
    var variable:String = "variable"
    let constant:String = "constant"
}
let constantStructSample = StructSample() // 構造体(値型)を初期化, 定数 に代入

constantStructSample.variable = "" // let constantStructSampleによる定数エラー
constantStructSample.constant = "" // let constant による定数エラー


var variableStructSample = StructSample() // 構造体(値型)を初期化, 定数 に代入

variableStructSample.variable = "" // エラーなし
variableStructSample.constant = "" // let constantによる定数エラー

let で定義したプロパティは、もちろん変更できない。

気になるのは var で定義したプロパティの変更をしようとしている以下のコードがエラーになる点だ。
これについて詳しく言及する。

let constantStructSample = StructSample() // 構造体(値型)を初期化, 定数 に代入
constantStructSample.variable = "" // let constantStructSampleによる定数エラー

struct のインスタンスは 値

まず struct は値型である。
structSample.variable = ""で、structSampleという に変更をかけたとしよう。

この時、「インスタンス」がletで定義されているものは、「クラスのプロパティ」がvarで定義されていたとしても、インスタンスそのものが定数 であるため、値の変更はできない。
中の variableが変数であっても、大枠のインスタンスが定数である場合、変更できないのだ。

class,参照型のインスタンスを変更してみる

class ClassSample{
    var variable:String = "variable"
    let constant:String = "constant"
}
let constantClassSample = ClassSample() // クラス(参照型)を初期化, 定数 に代入

constantClassSample.variable = "" // エラーなし (ここが変な感じ)
constantClassSample.constant = "" // let constantによるエラー


var variableClassSample = ClassSample() // クラス(参照型)を初期化, 変数 に代入

variableClassSample.variable = "" // エラーなし
variableClassSample.constant = "" // let constantによるエラー

let で定義したプロパティは、もちろん変更できない。

気になるのは、先ほどの struct で定義されたクラスのインスタンスで、定数で代入された以下のコードはエラーなのに、class の場合はエラーにならない点だ。

let constantStructSample = StructSample() // 構造体(値型)をinit(),定数に代入
constantStructSample.variable = "" // エラー

let constantClassSample = ClassSample() // クラス(参照型)をinit(),定数に代入
constantClassSample.variable = "" // エラーなし

class のインスタンスは 参照先

まず、class は参照型である。
let constantClassSample = ClassSample() とした時、constantClassSampleには何が入っているだろうか。
わかりやすい例を以下に用意した。

let structSample = StructSample() // 構造体(値型)をinit(),定数に代入
let classSample = ClassSample() // クラス(参照型)をinit(),定数に代入

print(structSample) // StructSample(variable: "variable", constant: "constant")
print(classSample) // __lldb_expr_400.ClassSample
console
StructSample(variable: "variable", constant: "constant")
__lldb_expr_400.ClassSample

値型のインスタンスは、値そのもの が入っていることが確認できる。
参照型のインスタンスは、インスタンスが存在するポインタアドレスが入っていることが確認できる。つまり、参照先 が代入されているのである。

let constantClassSample = ClassSample() // クラス(参照型)をinit()
var variableClassSample = ClassSample() // クラス(参照型)をinit()

ではこれらの違いはなんだろうか。
init() されたインスタンスを指してるポインタ(参照)のアドレスを、前者は定数に格納して、後者は変数に格納しているのである。

話を元に戻すと、

let constantClassSample = ClassSample() // 参照型をinit()して定数に格納
constantClassSample.variable = "" // 定数なのにエラーなし

が、エラーが出ないのは、このインスタンスを指してる 参照 を定数にしているだけで、値の変更はできるのだ。

計算プロパティ

値を保持せず、代わりに、間接的に他のプロパティの値を取得し、なんらかの処理を即時行って値を返す get を提供する。
他のプロパティに値を設定する set を提供することもできる。

struct Rect{
    var origin = Point()
    var size = Size()
    var center:Point {
        get{ // centerへのアクセスで、値を計算して返す
            let centerX = origin.x + size.width / 2
            let centerY = origin.y + size.height / 2
            return Point(x: centerX, y: centerY)
        }
        set(newCenter){ //centerに値を代入で、計算 -> origin書き換え
            origin.x = newCenter.x - size.width / 2
            origin.y = newCenter.y - size.height / 2
        }
    }
}
var square = Rect(
    origin: Point(x: 0.0, y: 0.0),
    size: Size(width: 10.0, height: 10.0)
)

var initialSquareCenter = square.center //getによる計算結果が返る
print(initialSquareCenter) //Point(x: 5.0, y: 5.0)

square.center = Point(x: 15, y: 15) //setによる書き換え
var newOrigin = square.origin //Point(x: 10.0, y: 10.0)

また、計算プロパティは以下のような省略・機能が利用できる。

  • getが単一式で構成される場合returnを省略できる。(暗黙的に単一式をreturnする)
  • setが、設定される新しい名前を定義しない場合、デフォルトのnewValueが使用可能。
struct CompactRect {
    var origin = Point()
    var size = Size()
    var center: Point {
        get {
            Point(x: origin.x + (size.width / 2),
                  y: origin.y + (size.height / 2))
        }
        set {
            origin.x = newValue.x - (size.width / 2)
            origin.y = newValue.y - (size.height / 2)
        }
    }
}

プロパティオブザーバ

プロパティオブザーバは、プロパティの値の変化を監視し、それに応じたアクションをすることができる。
プロパティにこれらのオブザーバの下記のいずれかまたは両方を定義可能。

  • willSet は、値が格納される直前に呼び出される。
  • didSet は、新しい値が格納された直後に呼び出される。
struct StepCounter{
    
    var number:Int = 0 {
        willSet{
            print("値(\(newValue))に変更されようとしています。")
        }
        didSet{
            print("値(\(oldValue))が,\(number)に変更されました。")
        }
    }
}
var counter = StepCounter()
counter.number = 1
値(1)に変更されようとしています。
値(0)が,1に変更されました。

willSetでは格納プロパティの値(この例ではnumber)・新しく代入しようとする値 を使用することができる。
didSetでは格納プロパティの値(この例ではnumber)・代入される前の値 を使用することができる。

以下の図は、willSet,didSetで値がどのように通知されるのかを流れで表したものである。

image.png

willSet / didSet は、「値の変更通知」であって「変更制御」ではない

「条件に合わなければプロパティの代入を止めたい」という時、willSet,didSetbreakを使用すると良さそうと考えてしまいがちだが、willSetdidSet では 代入をキャンセルすることはできない

Swiftの willSet/didSet は、代入される直前・直後に呼ばれるだけの通知的な仕組みであり、willSet は「今から newValue を代入する」と通知するだけの力しか持っていない。
また、didSet は「たった今、値の変更完了した」と通知するだけで、didSetのスコープ内で、値そのものをブロック(拒否)する力は持ってない。

また、breakを利用すると良さそうに感じるが、break は基本的に ループ(for, while)や switch の中でしか使用できないため、willSet の中に break 書いたら文法エラーになる。

条件を満たさない場合に代入を防ぐコードは、計算プロパティを使用するとうまくいく。

struct StepCounter {
    private(set) var currentCount = 0
    
    private var _number: Int = 0
    var number: Int {
        get { _number }
        set {
            if newValue >= 0 {
                currentCount += newValue
                _number = newValue
                print("値が変更されました。")
            } else {
                print("0未満は設定できません。")
            }
        }
    }
}
var counter = StepCounter()
counter.number = 10  // OK
counter.number = -5  // 拒否される
print(counter.currentCount) // 10(変わらない)

プロパティラッパー

プロパティラッパ(@propertyWrapper) は、共通のプロパティ管理ロジックを一箇所にまとめて、共通処理の再利用を便利にする仕組み。

@propertyWrapper
struct TwelveOrLess {
    private var number:Int = 0
    private var maximum:Int = 12
    
    var wrappedValue:Int{
        get{
            number
        }
        set{
            number = min(newValue,maximum)
        }
    }
}

@propertyWrapper を定義するには、必ず wrappedValue というプロパティを定義する必要がある。
この名前をキーにして、Swiftのコンパイラが、値(height, width)の読み書きの際に、 wrappedValue 経由で共通のプロパティ管理ロジック(TwelveOrLess)を適応させるからである。

struct SmallRectangle{
    @TwelveOrLess var height:Int
    @TwelveOrLess var width:Int
}

@propertyWrapperが指定されたheightの値は、wrappedValue経由で、プロパティ管理ロジック(TwelveOrLess)のチェックを受ける。要は set に値が流れ込む。

var rectangle = SmallRectangle()
rectangle.height = 18
print(rectangle.height) // 12

内部で起こること

プロパティラッパを変数に指定すると、コンパイラは以下の裏コードを自動生成する。

private var _height = TwelveOrLess() //デフォルト値(number=0,maximum=12)を利用した初期化
var height: Int {
    get { _height.wrappedValue }
    set { _height.wrappedValue = newValue }
}

このコードは見えないが、上記コードを裏側で定義してくれているおかげで、

  • heightにはTwelveOrLess()による初期化
  • さらに計算プロパティが組み込まれる( height が実体を持たず、中の _height.wrappedValue にアクセスする)

ことになる。@TwelveOrLess var height:Int とすることで、初期化計算プロパティ が隠蔽される形になる。

プロパティの初期値設定

上記のようなプロパティラッパーの定義では、任意の値を初期値として指定することはできない。
これは、プロパティラッパー側にイニシャライザ( init(wrappedValue:) )を用意していないからである

struct SmallRectangle {
    @TwelveOrLess var height: Int = 5  // エラーになる
}

こう書いてしまった場合、コンパイラは以下のような解釈をする。

private var _height = TwelveOrLess(wrappedValue: 5)

もし、初期値を指定したい場合はカスタムイニシャライザを実装する。

@propertyWrapper
struct TwelveOrLess {
    private var number:Int
    private var maximum:Int
    
    var wrappedValue:Int{
        get{
            number
        }
        set{
            number = min(newValue,maximum)
        }
    }
    
    init(wrappedValue:Int){
        maximum = 12
        number = min(wrappedValue,maximum)
    }
}
struct SmallRectangle{
    @TwelveOrLess var height:Int = 18
}

また、そのほかの引数を取る場合、カスタムイニシャライザを複数用意しておくことができる。

@propertyWrapper
struct TwelveOrLess {
    private var number:Int
    private var maximum:Int
    
    var wrappedValue:Int{
        get{
            number
        }
        set{
            number = min(newValue,maximum)
        }
    }
    
    
    init(wrappedValue:Int){
        maximum = 12
        number = min(wrappedValue,maximum)
    }
    
    init(wrappedValue:Int,maximum:Int){
        self.maximum = maximum
        number = min(wrappedValue,maximum)
    }
}

また、初期値指定の際に以下のような書き方ができる。特に引数が、wrappedValue以外にある場合、有効である。

struct SmallRectangle{
    @TwelveOrLess var height:Int = 18 // 引数がwrappedValueのみの場合
    @TwelveOrLess(wrappedValue: 200, maximum: 120) var width:Int // 引数が複数ある場合
}

イニシャライザは明示したほうがいい

Private initializer 'init(number:projectedValue:)' cannot have more restrictive access than its enclosing property wrapper type 'SmallNumber' (which is internal)
というエラーがたまにでる。
プロパティラッパー定義時に、

@propertyWrapper
struct TwelveOrLess {
    private var number = 0  // ← これが "デフォルトの初期値"

    var wrappedValue: Int {
        get { number }
        set { number = min(newValue, 12) }
    }
}

としたとき、イニシャライザは定義していないため、Swiftが暗黙的にイニシャライザを生成している。

init() {
    self.number = 0
}

ただたまに、暗黙的なイニシャライザがprivateレベルで自動生成される時があるらしい。
そのため、特別な理由がなければ init() は定義しておくが吉。これでイニシャライザもinternalレベルで定義される。(@propertyWrapperはデフォルトでinternalレベルなのでイニシャライザも@propertyWrapperと同じレベルに合わせておくということ。)

@propertyWrapper
struct SmallNumber {
    private var number = 0
    var wrappedValue: Int {
        get { return number }
        set {
            if newValue > 12 {
                number = 12
            } else {
                number = newValue
            }
        }
    }
    init(){} // 別にこれだけでもいい
}

もしくは、

@propertyWrapper
struct SmallNumber {
    private var number:Int
    var wrappedValue: Int {
        get { return number }
        set {
            if newValue > 12 {
                number = 12
            } else {
                number = newValue
            }
        }
    }
    init(){ // ちゃんと書いてみる
        number = 0
    }
}

projectedValueという追加情報

projectedValuewrappedValue 以外にSwiftが特別扱いする、もう一つのプロパティで、Swift が $プロパティ名 という特別な構文でアクセスさせてくれるラッパー側のもうひとつの出口。

@propertyWrapper
struct SmallNumber {
    private var number = 0
    var projectedValue = false
    var wrappedValue: Int {
        get { return number }
        set {
            if newValue > 12 {
                number = 12
                projectedValue = true
            } else {
                number = newValue
                projectedValue = false
            }
        }
    }
    init(){}
}
struct SomeStructure {
    @SmallNumber var someNumber: Int
}
var someStructure = SomeStructure()

someStructure.someNumber = 4
print(someStructure.$someNumber)
// false

someStructure.someNumber = 55
print(someStructure.$someNumber)
// true

また、ログを残すようなコードもかける。

@propertyWrapper
struct Loggable {
    private var value: String = ""
    private(set) var log: [String] = []

    var wrappedValue: String {
        get { value }
        set {
            value = newValue
            log.append("Set to: \(newValue)")
        }
    }

    var projectedValue: [String] {
        log  // ← Stringの配列
    }
}
struct User {
    @Loggable var name: String
}

var user = User()
user.name = "Taro"
user.name = "Jiro"

print(user.$name)  // ["Set to: Taro", "Set to: Jiro"]

少しまとめてみる

input 役割 output
wrappedValue 実際の値を外からアクセスする object.value
projectedValue 追加の情報や機能を$変数で提供 object.$value

応用例

自動トリミング(空白削除)

@propertyWrapper
struct Trimmed {
    private var value: String = ""

    var wrappedValue: String {
        get { value }
        set { value = newValue.trimmingCharacters(in: .whitespacesAndNewlines) }
    }
}
struct UserInput {
    @Trimmed var name: String
}

var input = UserInput()
input.name = "  Taro  "
print(input.name)  // → "Taro"

ログ出力付きプロパティ

@propertyWrapper
struct Logged<T> {
    private var value: T
    private let name: String

    init(wrappedValue: T, name: String) {
        self.value = wrappedValue
        self.name = name
    }

    var wrappedValue: T {
        get { value }
        set {
            print("\(name)\(value) から \(newValue) に変更しました")
            value = newValue
        }
    }
}
struct Settings {
    @Logged(name: "音量") var volume: Int = 5
}

var s = Settings()
s.volume = 8
// → 音量 を 5 から 8 に変更しました

UserDefault連携

@propertyWrapper
struct UserDefault<T> {
    let key: String
    let defaultValue: T

    var wrappedValue: T {
        get {
            UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
        }
        set {
            UserDefaults.standard.set(newValue, forKey: key)
        }
    }
}
struct Preferences {
    @UserDefault(key: "username", defaultValue: "guest")
    var username: String
}

var prefs = Preferences()
print(prefs.username)  // → "guest"(初期状態)
prefs.username = "nobu"
print(UserDefaults.standard.string(forKey: "username")!)  // → "nobu"

数値の範囲制御

@propertyWrapper
struct Clamped<Value: Comparable> {
    private var value: Value
    let range: ClosedRange<Value>

    init(wrappedValue: Value, _ range: ClosedRange<Value>) {
        self.range = range
        self.value = min(max(wrappedValue, range.lowerBound), range.upperBound)
    }

    var wrappedValue: Value {
        get { value }
        set { value = min(max(newValue, range.lowerBound), range.upperBound) }
    }
}
struct BrightnessSetting {
    @Clamped(0...100) var brightness: Int = 50
}

var setting = BrightnessSetting()
setting.brightness = 120
print(setting.brightness)  // → 100

UITextFieldの入力値を制御するプロパティラッパー

@propertyWrapper
struct MaxLength {
    private var value: String = ""
    private let limit: Int

    init(wrappedValue: String, _ limit: Int) {
        self.limit = limit
        self.value = String(wrappedValue.prefix(limit))
    }

    var wrappedValue: String {
        get { value }
        set { value = String(newValue.prefix(limit)) }
    }
}
Model
struct UserInput {
    @MaxLength(10) var name: String = ""
}
Controller
class ViewController: UIViewController {

    // View: UITextField を storyboard から接続
    @IBOutlet weak var nameTextField: UITextField!

    // Model
    var userInput = UserInput()

    override func viewDidLoad() {
        super.viewDidLoad()

        // イベント監視
        nameTextField.addTarget(self, action: #selector(textFieldDidChange(_:)), for: .editingChanged)
    }

    @objc func textFieldDidChange(_ textField: UITextField) {
        userInput.name = textField.text ?? ""
        print("入力された名前(制限済): \(userInput.name)")

        // 文字が切り詰められたら表示も更新してあげる
        if textField.text != userInput.name {
            textField.text = userInput.name
        }
    }
}

複数のラッパーを組み合わせることもできる。

@Trimmed
@Logged(name: "名前")
var name: String = ""

static キーワード

staticは「全インスタンスで共通の情報」を定義する際に便利である。また、staticを用いてプロパティを定義すると、アクセス時、インスタンス化が不要になる。

struct Roster{
    static var company = "Apple.inc"
    var name:String
}

社員個票用のクラスを作成した。
この時、company,nameにアクセスしてみよう。
通常の定義をしたnameプロパティは、インスタンス化をしてから.nameでアクセスする。

let employee = Roster(name: "Sam")
let employeeName = employee.name

一方、staticで定義したcompanyプロパティはインスタンス化が不要である。
companyプロパティは全インスタンスで共通の値であるため、インスタンスごとに用意するのではなく、クラス自体にただ一つ用意されたプロパティとして扱う。

アクセス時はクラス名.プロパティでアクセスする。

let companyName = Roster.company

staticはクラスにくっついてるって分かってるけど、共通であればインスタンスからアクセスできても自然では?

たとえば以下は許可されていない。

let employee = Roster(name: "Sam")
let companyName = employee.company //インスタンスからアクセス->(エラー)

Samの例を見てみよう。

let sam = Employee(name: "Sam")
print(sam.companyName) // ← 「Samの会社の名前」-> うーん (ちなみにこう書くとエラーが出る)

これは一見できるが、「会社はSamのもの」と勘違いされる可能性がある。
実際は、全社員で共有する会社名を表しているため、クラス名からアクセスするべき。(swiftらしい)

まとめ

  • struct(値型)は「まるごとコピー」だから、letで定義するとプロパティ変更も不可
  • class(参照型)は「アドレス参照」だから、letでもプロパティ変更OK
  • @propertyWrapper で共通処理を1箇所にまとめて、安全・シンプルに!
  • willSet / didSet はロジック補助やログ用途に便利
  • projectedValue$変数名)を使えば、裏の値や追加情報も取り出せる

疲れt...

0
1
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
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?