C#プログラミングにおいて、変数の値を設定する「代入」は、最も頻繁に行われる操作の一つです。

しかし、近年のC#は急速な進化を遂げており、単なる「左辺に右辺を代入する」という枠組みを超え、コードの簡潔性と安全性を高めるための多様な代入機能が備わっています。

基本的な代入演算子から、null安全を支える演算子、さらにはタプルを用いた分割代入や最新のコレクション式まで、その使いこなしは開発効率に直結します。

本記事では、C#における代入の仕組みを網羅的に解説し、実務で役立つテクニックを紹介します。

基本の代入演算子 (=)

C#で最も基本となるのは、等号 = を使用した代入演算子です。

これは「右辺の値を評価し、その結果を左辺の変数に格納する」という動作を行います。

一見単純ですが、代入されるデータの型が「値型」か「参照型」かによって、その挙動は大きく異なります。

値型と参照型による挙動の違い

値型(intdoublestruct など)の代入では、データそのものがコピーされます。

一方、参照型(classstringarray など)の代入では、メモリ上のデータへの「参照(アドレス)」のみがコピーされます。

C#
using System;

public class Program
{
    public static void Main()
    {
        // 値型の代入
        int a = 10;
        int b = a; // aの値がコピーされる
        b = 20;    // bを変更してもaには影響しない
        Console.WriteLine($"a: {a}, b: {b}");

        // 参照型の代入
        int[] arr1 = { 1, 2, 3 };
        int[] arr2 = arr1; // 参照(アドレス)がコピーされる
        arr2[0] = 99;      // arr2を通じた変更はarr1にも反映される
        Console.WriteLine($"arr1[0]: {arr1[0]}, arr2[0]: {arr2[0]}");
    }
}
実行結果
a: 10, b: 20
arr1[0]: 99, arr2[0]: 99

参照型の代入において、「実体は一つだが、それを指し示す変数が複数ある」という状態を理解することは、意図しないバグを防ぐために不可欠です。

型推論 (var) を用いた代入

C#では、代入時の右辺から型を推論する var キーワードを使用できます。

これにより、複雑な型名の記述を省略し、コードの可読性を向上させることが可能です。

C#
// 明示的な型指定
List<string> list1 = new List<string>();

// 型推論を利用した代入
var list2 = new List<string>(); 

// 右辺から型が明らかな場合に推奨される
var count = 10; // int
var message = "Hello"; // string

ただし、var はあくまでコンパイル時に型が決定されるものであり、動的な型付け(JavaScriptなどの letvar)とは異なる点に注意してください。

複合代入演算子

複合代入演算子は、演算と代入を一つのステップで行うためのショートカットです。

a = a + b と書く代わりに a += b と記述できます。

これにより、同じ変数を二度記述する手間が省け、コードがスッキリします。

算術およびビット演算の複合代入

よく使われる複合代入演算子を以下の表にまとめます。

演算子展開後の意味
+=a += ba = a + b
-=a -= ba = a - b
*=a *= ba = a * b
/=a /= ba = a / b
%=a %= ba = a % b
&=a &= ba = a & b (論理積/ビット積)
|=a |= ba = a | b (論理和/ビット和)
^=a ^= ba = a ^ b (排他的論理和)
<<=a <<= ba = a << b (左シフト)
>>=a >>= ba = a >> b (右シフト)

これらは数値計算だけでなく、文字列の連結 (+=) やイベントハンドラの登録などでも頻繁に利用されます。

C#
string text = "C#は";
text += "強力な言語です。"; // 文字列連結の複合代入

Console.WriteLine(text);
実行結果
C#は強力な言語です。

null合体代入演算子 (??=)

C# 8.0で導入された ??= 演算子は、「変数がnullの場合にのみ値を代入する」という操作を非常に簡潔に記述できる強力なツールです。

基本的な使い方

従来、変数がnullかどうかをチェックして初期値を設定する場合、if 文や三項演算子を使用する必要がありました。

C#
// 従来の書き方
if (name == null)
{
    name = "Default Name";
}

// ??= 演算子を使用した書き方
name ??= "Default Name";

この演算子を使用すると、左辺が null でない場合は何もしないため、不必要な代入や上書きを防止できます。

実践的な活用シーン(遅延初期化)

プロパティのゲッターなどで、必要になったタイミングで初めてインスタンスを生成する「遅延初期化(Lazy Initialization)」の実装に非常に適しています。

C#
using System;
using System.Collections.Generic;

public class DataService
{
    private List<string> _cachedData;

    public List<string> GetData()
    {
        // _cachedDataがnullなら、新しいリストを作成して代入
        return _cachedData ??= FetchFromDatabase();
    }

    private List<string> FetchFromDatabase()
    {
        Console.WriteLine("データベースから取得中...");
        return new List<string> { "Data1", "Data2" };
    }
}

このパターンを使用することで、コードのインデントを深くすることなく、安全に初期化処理を記述できます。

分割代入 (Destructuring)

C# 7.0以降、タプルやユーザー定義のオブジェクトを複数の変数へ一度に展開して代入する「分割代入」が可能になりました。

これにより、メソッドから複数の戻り値を受け取る際の処理が格段にスムーズになります。

タプルの分割代入

複数の値を一つのグループとして返すメソッドの結果を、個別の変数に直接代入できます。

C#
using System;

public class Program
{
    public static void Main()
    {
        // メソッドの戻り値を直接分割して代入
        var (name, age) = GetUserInfo();

        Console.WriteLine($"名前: {name}, 年齢: {age}");
    }

    static (string, int) GetUserInfo()
    {
        return ("田中", 25);
    }
}
実行結果
名前: 田中, 年齢: 25

不要な値がある場合は、アンダースコア _ を使った破棄(Discard)を利用することで、特定の変数だけを受け取ることができます。

C#
// 年齢だけが必要な場合
var (_, age) = GetUserInfo();

ユーザー定義型での分割代入 (Deconstruct)

自作のクラスや構造体においても、Deconstruct メソッドを定義することで分割代入に対応させることができます。

C#
public class Point
{
    public int X { get; }
    public int Y { get; }

    public Point(int x, int y) => (X, Y) = (x, y);

    // 分割代入を可能にするメソッド
    public void Deconstruct(out int x, out int y)
    {
        x = X;
        y = Y;
    }
}

// 利用例
var point = new Point(10, 20);
var (px, py) = point; // Deconstructが呼び出される

特殊な代入操作

C#には、特定のシナリオでパフォーマンスを最適化したり、安全性を高めたりするための特殊な代入方法が存在します。

参照代入 (ref assignment)

通常、代入は「値のコピー」ですが、ref キーワードを使用すると、変数そのものの「場所(エイリアス)」を代入することができます。

C#
int x = 1;
int y = 2;

// x または y の参照を保持する変数
ref int alias = ref x;

alias = 10; // alias(実体はx)を書き換える
Console.WriteLine($"x: {x}"); // xも10になる

alias = ref y; // 参照先をyに切り替え
alias = 20;
Console.WriteLine($"y: {y}"); // yも20になる
実行結果
x: 10
y: 20

これは大規模な構造体を頻繁に入れ替える場合や、配列の特定の要素を直接操作したい場合のパフォーマンスチューニングに有効です。

init専用プロパティと初期化

C# 9.0で導入された init アクセサを使用すると、オブジェクトの初期化時のみ代入を許可し、その後の変更を禁止するプロパティを作成できます。

C#
public class User
{
    public int Id { get; init; } // 初期化時のみ代入可能
    public string Name { get; set; }
}

var user = new User { Id = 1, Name = "Sato" };
// user.Id = 2; // コンパイルエラー:初期化後なので代入不可

これにより、イミュータブル(不変)なオブジェクト設計が容易になります。

with式による非破壊的代入

レコード型(record)や構造体では、既存のオブジェクトの一部の値だけを変更した新しいコピーを作成する with 式が利用できます。

これは「値を書き換える」のではなく、「新しい値を持った別個体を作る」という代入の新しい形です。

C#
public record PointRecord(int X, int Y);

var p1 = new PointRecord(10, 20);
// Xだけを書き換えた新しいインスタンスを作成
var p2 = p1 with { X = 99 };

Console.WriteLine(p1);
Console.WriteLine(p2);
実行結果
PointRecord { X = 10, Y = 20 }
PointRecord { X = 99, Y = 20 }

コレクション式と代入 (C# 12以降)

C# 12からは、配列やリストへの代入を統一的な記法で行える コレクション式 が導入されました。

これまで型によって異なっていた初期化プロセスの代入が、シンプルかつ直感的になっています。

C#
// 配列への代入
int[] array = [1, 2, 3, 4, 5];

// リストへの代入
List<string> colors = ["Red", "Green", "Blue"];

// スプレッド演算子(..)を用いた、別のコレクションの代入・結合
int[] extra = [0, ..array, 6];

スプレッド演算子 .. を使用すると、既存のコレクションを別のコレクションの代入プロセスの一部として展開できます。

これは、データのコピーや結合を伴う代入処理を非常に簡潔にします。

まとめ

C#における代入は、単純なデータのコピーに留まらず、プログラムの堅牢性と表現力を高めるための多彩な手段を提供しています。

  • 基本代入 (=): 値型と参照型の挙動の違いを意識することが重要。
  • 複合代入 (+=, ??=): コードの冗長性を排除し、nullチェックを簡略化。
  • 分割代入: タプルやオブジェクトから必要なデータを効率的に抽出。
  • 最新機能 (init, with, コレクション式): 不変性の維持や、直感的なデータ操作を実現。

これらの代入手法を適切に使い分けることで、可読性が高く、意図せぬ副作用の少ない高品質なC#コードを書くことができます。

まずは基本的な算術代入やnull合体代入から積極的に活用し、徐々に分割代入やコレクション式などの高度な機能を自身のコードに取り入れてみてください。