C#
 Computer >> コンピューター >  >> プログラミング >> C#

C#のref・out・inキーワードの使い方と違いを徹底解説

パラメータと引数の基本

C#では、ほとんどのメソッドが0個以上の「パラメータ」を持ち、メソッドに渡すべきデータを定義します。メソッドを呼び出すコード側は、このデータを「引数」として渡します。つまり、メソッドは入力をパラメータとして宣言し、呼び出し元が引数という形で実際の値を提供する仕組みです。

例として、次のメソッドとその呼び出しを見てみましょう。

static void Greet(string greeting){
    Console.WriteLine(greeting);
}
...
Greet("Hello");

上記の例では、greetingがGreet()メソッドのパラメータ、"Hello"がメソッドに渡された引数に相当します。

デフォルトの動作:値渡し

メソッドを呼び出して引数を渡す場合、C#ではデフォルトで「値渡し(pass by value)」が行われます。これは、メソッドに渡される時点で値のコピーが作られることを意味します。そのため、メソッド内で引数に行った変更は、元の変数には一切反映されません。

using System;
int number = 8;
Increase(number);
Console.WriteLine(number); // 8と表示される
static void Increase(int num){
    num = num + 1;
    Console.WriteLine(num); // 9と表示される
}

参照型を渡した場合

オブジェクトのような参照型の変数を渡す場合でも、C#は参照そのものを値としてコピーします。変数が保持しているのはオブジェクト本体ではなく参照だからです。しかし、コピーされた参照変数も元の変数も、メモリ上の同じオブジェクトを指しています。そのため、メソッド内の変数を通じてオブジェクトに加えた変更は、メソッド外の変数からも確認できます。

using System;
var john = new User{
    Name = "John",
    Salary = 50000
};
Promote(john);
Console.WriteLine(john.Salary); // 60000と表示される
static void Promote(User u){
    u.Salary += 10000;
    Console.WriteLine(u.Salary); // 60000と表示される
}

変数そのものを変更した場合

一方、メソッド内で変数の値そのものを変更しても、その変更はメソッド外には反映されません。変更されたのはあくまでコピーであり、実体ではないためです。たとえば、メソッド内で引数にnullを代入しても、呼び出し元の変数は元のまま変わりません。

using System;
var john = new User{
    Name = "John",
    Salary = 50000
};
Promote(john);
Console.WriteLine(john.Salary); // 50000と表示される
static void Promote(User u){
    u = null;
}

このような課題を解決するために、C#ではメソッドのパラメータを制御するための3つの修飾子キーワードが用意されています。

ref修飾子:明示的な参照渡し

ref修飾子を使うと、パラメータを参照渡しできます。渡す変数が参照型か値型かは問いません。パラメータと引数はどちらも同じメモリ位置を参照することになります。

次の例では、引数uにnullを設定すると、john変数もnullになるため、NullReferenceException(ヌル参照例外)がスローされます。

例:

using System;
var john = new User{
    Name = "John",
    Salary = 50000
};
Promote(ref john);
Console.WriteLine(john.Salary); // System.NullReferenceExceptionが発生
static void Promote(ref User u){
    u = null;
}

なお、ref修飾子を付けて渡す引数は、渡す前に必ず初期化されている必要があります。

out修飾子:メソッド内での初期化を前提とした参照渡し

out修飾子はref修飾子と似ていますが、以下の点が異なります。

  • 関数に渡す前に、引数を初期化しておく必要がない

  • 関数から抜ける前に、引数を必ず初期化(代入)しなければならない

次の例では、Hire関数がout修飾子経由で渡された新しいユーザーオブジェクトを初期化しています。注目すべきは、Hireメソッドの呼び出し時に変数johnがその場で宣言されている点です。

using System;
Hire(out User john);
Console.WriteLine(john.Salary); // 50000と表示される
static void Hire(out User u){
    u = new User{
        Name = "John",
        Salary = 50000
    };
}

ref修飾子と同様に、out修飾子が付いた変数も参照渡しされます。

out変数は、次のように関数から複数の戻り値を取得したい場合によく使われます。

using System;
var john = new User{
    Name = "John",
    Salary = 50000
};
bool shouldPromote = Raise(john.Salary, out double hike);
Console.WriteLine(shouldPromote); // True
Console.WriteLine($"昇給額 = {hike}"); // 5000と表示される
static bool Raise(int salary, out double hike){
    hike = salary * 0.1;
    return hike < 7000;
}

in修飾子:読み取り専用の参照渡し

in修飾子はrefoutパラメータと似ていますが、in引数を受け取ったメソッドはその値を変更できません。変更しようとすると、C#コンパイラがコンパイル時エラーを生成します。

inパラメータを使うと、大きな値型のメモリをパラメータ変数へコピーする処理をコンパイラが回避できるうえ、オブジェクトの意図しない変更も防げます。そのため、大きな値型をメソッドに渡す際に非常に有用です。

var point = new Coord();
Verify(point);
static void Verify(in Coord c){
    // エラー:readonly変数であるため 'in Coord' 変数には代入できません
    c = new Coord();
}
struct Coord{
    public int X;
    public int Y;
}

すべてを組み合わせた総合例

using System;
class Program{
    static void Main(){
        int number = 8;
        Increase(number);
        Console.WriteLine(number); // 8と表示される
        var john = new User { Name = "John", Salary = 50000 };
        Promote(john);
        Console.WriteLine(john.Salary); // 60000と表示される
        Leave(john);
        Console.WriteLine(john.Salary); // 60000と表示される

        LeaveByRef(ref john);
        Console.WriteLine(john?.Salary); // 何も表示されない

        User dave;
        Hire(out dave);
        Console.WriteLine(dave.Salary); // 50000と表示される
    }
    static void Increase(int num){
        num = num + 1;
        Console.WriteLine(num); // 9と表示される
    }
    static void Promote(User u){
        u.Salary += 10000;
        Console.WriteLine(u.Salary); // 60000と表示される
    }
    static void Leave(User u){
        u = null;
    }
    static void LeaveByRef(ref User u){
        u = null;
    }
    static void Hire(out User u){
        u = new User{
            Name = "John",
            Salary = 50000
        };
    }
}
class User{
    public string Name { get; set; }
    public int Salary { get; set; }
}

出力結果

9
8
60000
60000
60000
50000

まとめ:3つの修飾子の使い分け

修飾子特徴主な用途
ref参照渡し。呼び出し前の初期化が必須メソッド内の変更を呼び出し元に反映させたい場合
out参照渡し。呼び出し前の初期化は不要だが、メソッド内での代入が必須複数の戻り値を返したい場合
in参照渡し。メソッド内での変更不可(読み取り専用)大きな値型をコピーせずに安全に渡したい場合

これらの修飾子を適切に使い分けることで、意図しない副作用を防ぎながら、パフォーマンスと可読性に優れたコードを書くことができます。

  1. C#のrefパラメーターとoutパラメーターの違いとは?使い分けのポイントを解説

    C#には、メソッドに引数を渡す際に変数そのものへの参照を渡すための仕組みとして、「refパラメーター」と「outパラメーター」の2種類が用意されています。どちらも呼び出し元の変数の値をメソッド内から直接操作できる点は共通していますが、初期化の要否や代入のルール、想定される用途に違いがあります。本記事では、サンプルコードを交えながら、両者の違いをわかりやすく解説します。 refパラメーター(参照パラメーター)とは refパラメーターは、変数のメモリ上の場所(参照)をそのまま渡す仕組みです。通常の値渡しとは異なり、新しいストレージ領域は作成されません。そのため、メソッド内で行った変更が、そのまま呼

  2. Pythonにおける「+=」と「=+」の違いとは?演算子の動作をわかりやすく解説

    Pythonには見た目がよく似ているのに、まったく異なる動作をする演算子が存在します。その代表例が「+=」と「=+」です。この2つは一見同じように見えますが、実際には意味も役割も大きく違います。本記事では、それぞれの動作の仕組みと具体的な使い方を詳しく解説します。 「+=」は複合代入演算子(in-place演算) +=は、Pythonでは複合代入演算子と呼ばれるもので、内部的には object.__iadd__() メソッドへのシンタックスシュガー(糖衣構文)です。 公式ドキュメントには次のように記載されています。 これらのメソッドは、拡張された算術代入(+=、-=、*=、@=、/=、//=