{ }codelper

学ぶ › C#

言語ごとの特徴 · 36

構造体

関係のある値を一つにまとめ、そのまとまりがコピーされるのか共有されるのかを知ります。

この単元は C# · C++ · C 向けです。
Python・JavaScript・TypeScript・Java はクラスとオブジェクトでデータをまとめ(クラスの単元)、Craykon は 사전() の辞書でまとめます。
どれも代入すると参照で同じオブジェクトを共有するので、コピーされる構造体とは違います。

点の x・y、生徒の名前・点数のように、いつも一緒に動く値は構造体(struct)にまとめます。
下の例は点をコピーしてコピーだけを変え、元を動かして、コピーと元の違いを見せます。

using System; struct Point  // 값 타입: class로 바꾸면 q = p 가 같은 객체를 가리켜요{    public int X, Y;    public void Move(int dx, int dy) { X += dx; Y += dy; }    public int Len2() => X * X + Y * Y;    public override string ToString() => $"({X}, {Y})";} class Program{    static void Main()    {        string[] t = Console.ReadLine().Split();        var p = new Point { X = int.Parse(t[0]), Y = int.Parse(t[1]) };        Console.WriteLine($"p = {p}");         Point q = p;  // 데이터가 복사돼요        q.X = 10;        Console.WriteLine($"q = {q}");        Console.WriteLine($"p = {p}");         p.Move(1, 1);        Console.WriteLine($"p = {p}");        Console.WriteLine($"len2 = {p.Len2()}");    }}
stdin
3 4
stdout
p = (3, 4)
q = (10, 4)
p = (3, 4)
p = (4, 5)
len2 = 41

ポイント

  • ひとまとまりが新しい型になります。
    変数 1 つとして渡したり、返したり、配列に入れたりできます。
  • 中の値はメンバ(フィールド)といい、p.x のようにドットで取り出します。
  • 構造体を代入したり関数に渡したりすると、メンバがまるごとコピーされます。
    なので q = p の後に q.x = 10 としても、p は (3, 4) のままです。
  • 元を変えるにはポインタか参照で渡します。
    大きな構造体はコピーが高くつくので、読むだけでも const のポインタ・参照で渡すことがよくあります。

言語ごとの書き方

C#struct は値型なので Point q = p; はデータをコピーし、class は参照型なので同じ代入で同じオブジェクトを指します(例を class Point にすると q.X = 10 の後 p も (10, 4))— int・double・DateTime も構造体です。
座標・色・日付のような小さな値は struct(変えられないようにするなら readonly struct や record struct)、それ以外は class。
元を渡すなら ref
C++typedef なしでそのまま Point。
中にメンバ関数を置けて(void move(int dx, int dy))、変更しない関数には int len2() const のように const を付けます — struct と class の違いは既定の公開範囲(struct は public、class は private)だけです。
元は Point& や const Point& で渡します
Cstruct Point { int x, y; }; は使うたびに struct Point p; と書く必要があるので、ふつうは typedef struct { int x, y; } Point; — 元を変えるなら move(&p, 1, 1)、ポインタからメンバへは (*p).x の代わりに p->x、読むだけなら const Point *

よくある間違い

リストの中の構造体のメンバを変えたC#

var list = new List<Point> { new Point() };
list[0].X = 5;

→ コンパイルエラー CS1612 — インデクサがコピーを返すからです。配列の arr[0].X = 5 はできますが、foreach で取り出した構造体のメンバも変えられません(CS1654)

コピーを取り出して変えてから戻します:var t = list[0]; t.X = 5; list[0] = t;

構造体を == で比べた

if (p == q) { … }

→ C ではコンパイルエラーです。C++ は operator== がなければ、C# の struct は == を定義していなければ(CS0019)コンパイルエラーです

C はメンバを 1 つずつ比べます(memcmp はメンバの間のパディングのせいで間違うことがあります)。
C++20 は bool operator==(const Point&) const = default;、C# は p.Equals(q)(メンバの値を比較)か、== まで作ってくれる record struct。

値で受け取ってコピーだけを動かしたC · C++

void move(Point p, int dx, int dy) {
    p.x += dx;
    p.y += dy;
}

→ 呼び出し側の p はそのままです。エラーも警告もありません

C は Point *、C++ は Point& で受け取ります。

言語ごとの違い
構造体を渡したり代入したりするたびに、コピーなのか同じものなのかを確かめましょう。
C・C++・C# の struct はコピーなので、元を変えるにはポインタ(Point *)・参照(Point&)・C# の ref が必要で、C# の class は同じオブジェクトを共有します。

練習問題