Swift 言語ガイド – 9. 構造体とクラス

Swift

はじめに

公式ページのSwift 言語ガイド「The Swift Programming Language Swift 5.3」に基づき記載いたします。本ページでは、「構造体とクラス」について記載いたします。
記載内容に誤り等ございましたら、ご連絡をいただければ幸いです。

9. 構造体とクラス

構造体とクラスは、プログラムのコードの構成要素となる一般的な用途の柔軟な構造体です。定数、変数、および関数の定義に使用するのと同じ構文を使用して、構造体とクラスに機能を追加するためプロパティとメソッドを定義します。

他のプログラミング言語とは異なり、Swiftではカスタム構造体とクラス用に個別のインターフェイスと実装ファイルを作成する必要はありません。Swiftでは、構造体またはクラスを1つのファイルで定義すると、そのクラスまたは構造体への外部インターフェイスが自動的に他のコードで使用できるようになります。

注意
クラスのインスタンスは、伝統的なオブジェクトとして知られています。しかしながら、Swiftの構造体とクラスは、他の言語よりも機能がはるかに近く、この章の多くでは、クラスまたは構造体型のインスタンスに適用される機能について説明しています。このため、より一般的な用語のインスタンスが使用されます。

9.1. 構造体とクラスの比較

Swiftの構造体とクラスには多くの共通点があります。

  • 値を格納するプロパティを定義する
  • 機能を提供するメソッドを定義する
  • 添え字構文を使用して値へのアクセスを提供する添え字を定義します
  • 初期状態を設定するための初期化子を定義します
  • デフォルトの実装に加えて、その機能を拡張する
  • プロトコルに準拠して、特定の種類の標準機能を提供します

詳細については、プロパティ、メソッド、添え字、初期化、拡張機能、およびプロトコルを参照してください。

クラスには、構造体にはない追加の機能があります。

  • 継承により、あるクラスが別のクラスの特性を継承できます。
  • 型キャストを使用すると、実行時にクラスインスタンスの型を確認して解釈できます。
  • デイニシャライザーを使用すると、クラスのインスタンスで、割り当てられているリソースを解放できます。
  • 参照カウントでは、クラスインスタンスへの複数の参照が可能です。

詳細については、継承、型キャスト、デイニシャライザー、および自動参照カウントを参照してください。

クラスがサポートする追加機能には、複雑さが増すという犠牲が伴います。一般的なガイドラインとして、構造を優先するのは、推論が容易であるためです。クラスが適切または必要な場合は、クラスを使用します。実際には、これは、定義するカスタムデータ型のほとんどが構造体と列挙型であることを意味します。より詳細な比較については、構造体とクラスの選択を参照してください。

9.2. 構文定義

構造体とクラスの構文定義は似ています。structキーワードを使用して構造体を導入し、classキーワードを使用してクラスを導入します。どちらも、定義全体を中括弧のペア内に配置します。

struct SomeStructure {
    // 構造体の定義をここに記載します
}
class SomeClass {
    // クラスの定義をここに記載します
}

注意
新しい構造体またはクラスを定義するときはいつでも、新しいSwift型を定義します。標準のSwift型(String, Int, Boolなど)の大文字と一致するように、型にUpperCamelCase名(ここではSomeStructureやSomeClassなど)を付けます。プロパティとメソッドにlowerCamelCase名(frameRateやincrementCountなど)を付けて、型名と区別します。

構造体定義とクラス定義の例を次に示します。

struct Resolution {
    var width = 0
    var height = 0
}

class VideoMode {
    var resolution = Resolution()
    var interlaced = false
    var frameRate = 0.0
    var name: String?
}

上記の例では、Resolutionと呼ばれる新しい構造体を定義して、ピクセルベースのディスプレイ解像度を記述しています。この構造体には、widthとheightという2つのプロパティがあります。格納されるプロパティは、構造体またはクラスの一部としてバンドルおよび格納される定数または変数です。これらの2つのプロパティは、初期整数値0に設定することにより、Int型であると推測されます。

上記の例では、ビデオ表示用の特定のビデオモードを記述するために、VideoModeと呼ばれる新しいクラスも定義しています。このクラスには、4つのプロパティを格納した変数があります。最初のresolutionは、Resolutionのプロパティ型を推測する新しいResolution構造体のインスタンスで初期化されます。他の3つのプロパティについては、新しいVideoModeインスタンスは、interlacedがfalseに設定され(「非インターレースビデオ」を意味する)、0.0の再生フレームレート、およびnameと呼ばれるオプショナルの文字列値で初期化されます。nameプロパティはオプショナルの型であるため、デフォルト値のnilまたは「nonamevalue」が自動的に与えられます。

9.3. 構造体とクラスのインスタンス

Resolution構造体の定義とVideoModeクラスの定義は、ResolutionまたはVideoModeがどのように見えるかを説明するだけです。それら自体は、特定の解像度やビデオモードについては説明していません。そのためには、構造体またはクラスのインスタンスを作成する必要があります。

インスタンスを作成するための構文は、構造体とクラスの両方で非常に似ています。

let someResolution = Resolution()
let someVideoMode = VideoMode()

構造体とクラスはどちらも、新しいインスタンスに初期化構文を使用します。初期化構文の最も単純な形式では、クラスまたは構造体の型名の後に、Resolution()やVideoMode()などの空の括弧が続きます。これにより、クラスまたは構造体の新しいインスタンスが作成され、プロパティはデフォルト値に初期化されます。クラスと構造体の初期化については、初期化で詳しく説明しています。

9.4. プロパティへのアクセス

ドット構文を使用して、インスタンスのプロパティにアクセスできます。ドット構文では、インスタンス名の直後に、スペースを入れずにピリオド(.)で区切ってプロパティ名を記述します。

print("The width of someResolution is \(someResolution.width)")
// "The width of someResolution is 0"を出力します

この例では、someResolution.widthはsomeResolutionのwidthプロパティを参照し、デフォルトの初期値0を返します。

VideoModeのresolutionプロパティのwidthプロパティなど、サブプロパティにドリルダウンできます。

print("The width of someVideoMode is \(someVideoMode.resolution.width)")
// "The width of someVideoMode is 0"を出力します

ドット構文を使用して、変数プロパティに新しい値を割り当てることもできます。

someVideoMode.resolution.width = 1280
print("The width of someVideoMode is now \(someVideoMode.resolution.width)")
// "The width of someVideoMode is now 1280"を出力します

9.5. 構造体型のメンバーごとのイニシャライザー

すべての構造体には、自動生成されたメンバーごとの初期化子があります。これを使用して、新しい構造体インスタンスのメンバープロパティを初期化できます。新しいインスタンスのプロパティの初期値は、名前でメンバーごとの初期化子に渡すことができます。

let vga = Resolution(width: 640, height: 480)

構造体とは異なり、クラスインスタンスはデフォルトのメンバーごとの初期化子を受け取りません。イニシャライザーについては、初期化で詳しく説明しています。

9.6. 構造体と列挙型は値型

値型は、変数または定数に割り当てられたとき、または関数に渡されたときに値がコピーされる型です。

これまでの章では、実際に値型を幅広く使用してきました。実際、Swiftの基本的な型(整数、浮動小数点数、ブール値、文字列、配列、辞書)はすべて値型であり、舞台裏で構造体として実装されています。

すべての構造体と列挙型は、Swiftの値型です。つまり、作成した構造体と列挙型のインスタンス、およびそれらがプロパティとして持つ値型は、コードで渡されるときに常にコピーされます。

注意
配列、辞書、文字列などの標準ライブラリによって定義されたコレクションは、最適化を使用してコピーのパフォーマンスコストを削減します。これらのコレクションは、すぐにコピーを作成する代わりに、元のインスタンスとコピーとの間で要素が格納されているメモリを共有します。コレクションのコピーの1つが変更された場合、要素は変更の直前にコピーされます。コードに表示される動作は、常にコピーがすぐに行われたかのようです。

前の例のResolution構造体を使用するこの例について考えてみます。

let hd = Resolution(width: 1920, height: 1080)
var cinema = hd

この例では、hdという定数を宣言し、その定数にフルHDビデオの幅と高さ(幅1920ピクセル、高さ1080ピクセル)で初期化されたResolutionインスタンスを設定します。
次に、cinemaという変数を宣言し、hdの現在の値に設定します。Resolutionは構造体であるため、既存のインスタンスのコピーが作成され、この新しいコピーがcinemaに割り当てられます。hdとcinemaの幅と高さは同じになりますが、舞台裏ではまったく異なる2つのインスタンスです。

次に、cinemaのwidthプロパティは、デジタルシネマ映画に使用されるわずかに広い2K標準の幅(幅2048ピクセル、高さ1080ピクセル)に修正されます。

cinema.width = 2048

cinemaのwidthプロパティを確認すると、実際に2048に変更されていることがわかります。

print("cinema is now \(cinema.width) pixels wide")
// "cinema is now 2048 pixels wide"を出力します

しかしながら、元のhdインスタンスのwidthプロパティには、1920という古い値が残っています。

print("hd is still \(hd.width) pixels wide")
// "hd is still 1920 pixels wide"を出力します

cinemaにhdの現在の値が与えられると、hdに保存されている値が新しいcinemaインスタンスにコピーされました。最終結果は、同じ数値を含む2つの完全に別個のインスタンスでした。しかしながら、これらは個別のインスタンスであるため、次の図に示すように、cinemaのwidthを2048に設定しても、hdに保存されているwidthには影響しません。

同じ動作が列挙型に適用されます。

enum CompassPoint {
    case north, south, east, west
    mutating func turnNorth() {
        self = .north
    }
}
var currentDirection = CompassPoint.west
let rememberedDirection = currentDirection
currentDirection.turnNorth()

print("The current direction is \(currentDirection)")
print("The remembered direction is \(rememberedDirection)")
// "The current direction is north"を出力します
// "The remembered direction is west"を出力します

RememberedDirectionにcurrentDirectionの値が割り当てられると、実際にはその値のコピーが設定されます。その後、currentDirectionの値を変更しても、rememberedDirectionに保存されていた元の値のコピーには影響しません。

9.7. クラスは参照型

値型とは異なり参照型は、変数または定数に割り当てられたとき、または関数に渡されたときにコピーされません。コピーではなく、同じ既存のインスタンスへの参照が使用されます。

上記で定義したVideoModeクラスを使用した例を次に示します。

let tenEighty = VideoMode()
tenEighty.resolution = hd
tenEighty.interlaced = true
tenEighty.name = "1080i"
tenEighty.frameRate = 25.0

この例では、tenEightyという新しい定数を宣言し、VideoModeクラスの新しいインスタンスを参照するように設定します。ビデオモードには、以前から1920 x1080のHD解像度のコピーが割り当てられています。それは、インターレースに設定され、名前は「1080i」に設定され、フレームレートは25.0フレーム/秒に設定されています。

次に、tenEightyがalsoTenEightyという新しい定数に割り当てられ、alsoTenEightyのフレームレートが変更されます。

let alsoTenEighty = tenEighty
alsoTenEighty.frameRate = 30.0

クラスは参照型であるため、tenEightyとalsoTenEightyは実際には両方とも同じVideoModeインスタンスを参照します。事実上、次の図に示すように、これらは同じ単一インスタンスの2つの異なる名前にすぎません。

tenEightyのframeRateプロパティを確認すると、基になるVideoModeインスタンスからの新しいフレームレート30.0が正しく報告されていることがわかります。

print("The frameRate property of tenEighty is now \(tenEighty.frameRate)")
// "The frameRate property of tenEighty is now 30.0"を出力します

この例は、参照型の推論がいかに難しいかを示しています。プログラムのコードでtenEightyとalsoTenEightyが大きく離れている場合、ビデオモードが変更されるすべての方法を見つけるのは難しいかもしれません。tenEightyを使用する場合は常に、TenEightyも使用するコードについても考慮する必要があります。その逆も同様です。対照的に、同じ値と相互作用するすべてのコードがソースファイル内で近接しているため、値型について推論するのは簡単です。

tenEightyとalsoTenEightyは、変数ではなく定数として宣言されていることに注意してください。ただし、tenEightyおよびalsoTenEighty定数自体の値は実際には変更されないため、tenEighty.frameRateおよびalsoTenEighty.frameRateを変更することはできます。tenEightyとTenEighty自体は、VideoModeインスタンスを「保存」しません。代わりに、どちらも舞台裏でVideoModeインスタンスを参照します。変更されるのは、基になるVideoModeのframeRateプロパティであり、そのVideoModeへの定数参照の値ではありません。

9.8. 識別演算子

クラスは参照型であるため、複数の定数と変数が舞台裏でクラスの同じ単一のインスタンスを参照する可能性があります。(構造体や列挙型については、定数や変数に割り当てられたとき、または関数に渡されたときに常にコピーされるため、同じことは当てはまりません。)

2つの定数または変数がクラスのまったく同じインスタンスを参照しているかどうかを確認すると便利な場合があります。これを有効にするために、Swiftは2つのID演算子を提供します。

  • 同一(===)
  • 同一ではありません(!==)

これらの演算子を使用して、2つの定数または変数が同じ単一のインスタンスを参照しているかどうかを確認します。

if tenEighty === alsoTenEighty {
    print("tenEighty and alsoTenEighty refer to the same VideoMode instance.")
}
// "tenEighty and alsoTenEighty refer to the same VideoMode instance."を出力します

同一(3つの等号または===で表される)は、等しい(2つの等号または==で表される)と同じことを意味しないことに注意してください。

同一とは、クラス型の2つの定数または変数がまったく同じクラスインスタンスを参照することを意味します。等しいとは、型の設計者によって定義されているように、2つのインスタンスの値が等しい、または等しいと見なされることを意味します。

独自のカスタム構造体とクラスを定義する場合、2つのインスタンスが等しいと見なされるものを決定するのはあなたの責任です。==および!=演算子の独自の実装を定義するプロセスは、等価演算子で説明されています。

9.9. ポインタ

C、C++、またはObjective-Cの経験がある場合は、これらの言語がポインタを使用してメモリ内のアドレスを参照していることをご存知かもしれません。ある参照型のインスタンスを参照するSwift定数または変数は、Cのポインタに似ていますが、メモリ内のアドレスへの直接ポインタではなく、参照を作成していることを示すためにアスタリスク(*)を記述する必要はありません。代わりに、これらの参照は、Swiftの他の定数または変数と同様に定義されます。標準ライブラリには、ポインタを直接操作する必要がある場合に使用できるポインタとバッファの型が用意されています。「手動メモリ管理」を参照してください。

タイトルとURLをコピーしました