C#の値渡しとは?参照渡しとの違い・ref/out・参照型で混乱しないための基本を図解付きで解説
はじめに
C#のメソッド引数を理解するときに、最初につまずきやすいのが「値渡し」と「参照渡し」の違いです。
特にC#では、intやboolのような値型だけでなく、class、配列、List<T>のような参照型も、通常は値渡しでメソッドに渡されます。
ここで混乱しやすいのは、次のようなケースです。
C#void AddItem(List<string> list)
{
list.Add("Apple");
}
このコードでは、メソッド内でListを変更すると、呼び出し元のリストにも変更が反映されます。
そのため、「Listは参照渡しされているのでは?」と思うかもしれません。
しかし、正確にはこれは参照型の値渡しです。
C#の値渡しを理解するポイントは、次の2つです。
変数そのものがコピーされるのか
オブジェクトへの参照がコピーされるのか
この記事では、C#の値渡しについて、値型・参照型・ref・out・inの違いを、サンプルコードと図解を使ってわかりやすく解説します。
1. C#の値渡しとは?まず結論からわかりやすく解説
1-1. 値渡しとは「変数の中身をコピーしてメソッドに渡す」こと
C#の値渡しとは、メソッドを呼び出すときに、変数の中身をコピーして引数に渡す仕組みのことです。
たとえば、次のようなメソッドがあるとします。
C#void ChangeNumber(int x)
{
x = 100;
}
このメソッドに変数を渡しても、元の変数そのものが渡されるわけではありません。
C#int number = 10;
ChangeNumber(number);
Console.WriteLine(number); // 10
ChangeNumberメソッドの中でxに100を代入していますが、呼び出し元のnumberは10のままです。
これは、numberの値である10がコピーされて、メソッド内の引数xに渡されているためです。
1-2. C#の引数は基本的に値渡し
C#では、メソッドの引数は基本的に値渡しです。
C#void Method(int value)
{
// valueは呼び出し元の変数とは別物
}
この場合、valueは呼び出し元の変数そのものではなく、コピーされた値を受け取るための別の変数です。
ただし、ref、out、inを使うと、値渡しではなく参照渡しに近い動作になります。
C#void Method(ref int value)
{
value = 100;
}
refを付けると、呼び出し元の変数そのものをメソッド内で変更できます。
つまり、C#では次のように考えると整理しやすいです。
通常の引数 → 値渡し
ref / out / in → 参照渡し系の引数
1-3. 値渡しで元の変数が変わらない基本例
まずは、もっとも基本的な例を見てみましょう。
C#using System;
class Program
{
static void Main()
{
int a = 10;
Change(a);
Console.WriteLine(a); // 10
}
static void Change(int x)
{
x = 20;
}
}
このコードでは、Mainメソッドの変数aをChangeメソッドに渡しています。
Changeメソッドの中では、引数xに20を代入しています。
しかし、実行結果は次のようになります。
10
なぜなら、xはaのコピーだからです。
x = 20;によって変わるのは、メソッド内のxだけです。
呼び出し元のaは変更されません。
1-4. 図解:呼び出し元の変数とメソッド内の引数は別物
値渡しでは、呼び出し元の変数とメソッド内の引数は別の変数です。
呼び出し元
+--------+
| a = 10 |
+--------+
|
| 値をコピー
v
メソッド内
+--------+
| x = 10 |
+--------+
メソッド内でxを変更しても、aには影響しません。
呼び出し元
+--------+
| a = 10 |
+--------+
メソッド内
+--------+
| x = 20 |
+--------+
このように、値渡しでは「コピーされた別の変数を操作している」と考えると理解しやすくなります。
2. 値型の値渡し:int・bool・structで何が起きるのか
2-1. intを値渡しした場合のサンプルコード
intはC#の代表的な値型です。
値型を値渡しすると、値そのものがコピーされます。
C#using System;
class Program
{
static void Main()
{
int count = 5;
Increment(count);
Console.WriteLine(count); // 5
}
static void Increment(int value)
{
value++;
Console.WriteLine(value); // 6
}
}
実行結果は次のとおりです。
6
5
Incrementメソッド内ではvalueが6になります。
しかし、呼び出し元のcountは5のままです。
2-2. メソッド内で値を変更しても呼び出し元に影響しない理由
値型の値渡しでは、変数に入っているデータそのものがコピーされます。
C#int count = 5;
Increment(count);
このとき、countそのものがIncrementメソッドに渡されるわけではありません。
countの中に入っている5という値がコピーされ、メソッド内のvalueに入ります。
そのため、メソッド内で次のように変更しても、
C#value++;
変更されるのはコピー先のvalueだけです。
呼び出し元のcountは別の変数なので、影響を受けません。
2-3. structを値渡しした場合の注意点
structも値型です。
そのため、structを値渡しすると、構造体のデータがコピーされます。
C#using System;
struct Point
{
public int X;
public int Y;
}
class Program
{
static void Main()
{
Point p = new Point { X = 10, Y = 20 };
Move(p);
Console.WriteLine($"{p.X}, {p.Y}"); // 10, 20
}
static void Move(Point point)
{
point.X = 100;
point.Y = 200;
}
}
Moveメソッド内でpoint.Xとpoint.Yを変更していますが、呼び出し元のpは変わりません。
実行結果は次のようになります。
10, 20
Pointはstructなので、Move(p)の時点でpの内容がコピーされます。
メソッド内のpointは、呼び出し元のpとは別の構造体です。
ただし、大きなstructを頻繁に値渡しすると、コピーのコストが問題になる場合があります。
そのような場合には、inを使って読み取り専用で参照渡しすることがあります。
2-4. 図解:値型はデータそのものがコピーされる
値型の値渡しでは、データそのものがコピーされます。
呼び出し元
+----------------+
| p |
| X = 10 |
| Y = 20 |
+----------------+
|
| データをコピー
v
メソッド内
+----------------+
| point |
| X = 10 |
| Y = 20 |
+----------------+
メソッド内でpointを変更しても、呼び出し元のpは変わりません。
呼び出し元
+----------------+
| p |
| X = 10 |
| Y = 20 |
+----------------+
メソッド内
+----------------+
| point |
| X = 100 |
| Y = 200 |
+----------------+
値型では、「データそのものがコピーされる」と覚えておきましょう。
3. 参照型の値渡し:class・配列・Listで混乱しやすいポイント
3-1. 参照型でも「引数そのもの」は値渡しされる
C#で混乱しやすいのが、参照型をメソッドに渡す場合です。
参照型には、たとえば次のようなものがあります。
class配列
List<T>Dictionary<TKey, TValue>string
参照型の変数には、オブジェクトそのものではなく、オブジェクトを指すための参照が入っています。
そしてC#では、参照型の変数を通常の引数として渡す場合でも、引数そのものは値渡しです。
つまり、コピーされるのはオブジェクト本体ではなく、オブジェクトへの参照です。
3-2. 参照型ではオブジェクトへの参照がコピーされる
次の例を見てみましょう。
C#class Person
{
public string Name;
}
このPersonクラスをメソッドに渡します。
C#using System;
class Person
{
public string Name;
}
class Program
{
static void Main()
{
Person person = new Person { Name = "Alice" };
ChangeName(person);
Console.WriteLine(person.Name); // Bob
}
static void ChangeName(Person p)
{
p.Name = "Bob";
}
}
実行結果は次のようになります。
Bob
この結果を見ると、「参照渡しなのでは?」と思うかもしれません。
しかし、これは参照渡しではありません。
personに入っている「オブジェクトへの参照」がコピーされて、引数pに渡されています。
personとpは別の変数ですが、同じオブジェクトを指しているため、p.Nameを変更すると、呼び出し元から見えるオブジェクトの中身も変わります。
3-3. オブジェクトの中身を変更すると呼び出し元にも影響する
参照型の値渡しでは、参照がコピーされるため、呼び出し元とメソッド内の引数が同じオブジェクトを指すことがあります。
C#static void ChangeName(Person p)
{
p.Name = "Bob";
}
このコードは、引数pそのものを書き換えているのではなく、pが指しているオブジェクトのNameを変更しています。
つまり、変更している対象は次の部分です。
変数pではなく、pの先にあるオブジェクト
図で表すと、次のようなイメージです。
呼び出し元 ヒープ上のオブジェクト
+------------+ +----------------+
| person ----|----------> | Name = Alice |
+------------+ +----------------+
メソッド内
+------------+
| p ---------|----------> 同じオブジェクト
+------------+
p.Name = "Bob";を実行すると、同じオブジェクトの中身が変わります。
呼び出し元 ヒープ上のオブジェクト
+------------+ +----------------+
| person ----|----------> | Name = Bob |
+------------+ +----------------+
メソッド内
+------------+
| p ---------|----------> 同じオブジェクト
+------------+
そのため、呼び出し元のperson.NameもBobになります。
3-4. 引数にnewを代入しても呼び出し元の参照先は変わらない
一方で、メソッド内で引数そのものに別のオブジェクトを代入しても、呼び出し元の変数は変わりません。
C#using System;
class Person
{
public string Name;
}
class Program
{
static void Main()
{
Person person = new Person { Name = "Alice" };
ReplacePerson(person);
Console.WriteLine(person.Name); // Alice
}
static void ReplacePerson(Person p)
{
p = new Person { Name = "Bob" };
}
}
実行結果は次のようになります。
Alice
ReplacePersonメソッド内では、pに新しいPersonオブジェクトを代入しています。
しかし、pはpersonのコピーです。
そのため、pの参照先を変更しても、呼び出し元のpersonの参照先は変わりません。
3-5. 図解:参照型の値渡しは「参照のコピー」が渡される
参照型の値渡しでは、オブジェクトそのものではなく、参照がコピーされます。
呼び出し元 ヒープ
+------------+ +----------------+
| person ----|----------> | Name = Alice |
+------------+ +----------------+
|
| 参照をコピー
v
メソッド内
+------------+
| p ---------|----------> 同じオブジェクト
+------------+
この状態でオブジェクトの中身を変更すると、呼び出し元にも影響します。
C#p.Name = "Bob";
呼び出し元 ヒープ
+------------+ +----------------+
| person ----|----------> | Name = Bob |
+------------+ +----------------+
メソッド内
+------------+
| p ---------|----------> 同じオブジェクト
+------------+
しかし、引数pに新しいオブジェクトを代入しても、呼び出し元のpersonは変わりません。
C#p = new Person { Name = "Carol" };
呼び出し元 ヒープ
+------------+ +----------------+
| person ----|----------> | Name = Bob |
+------------+ +----------------+
メソッド内 ヒープ
+------------+ +----------------+
| p ---------|----------> | Name = Carol |
+------------+ +----------------+
この違いが、C#の値渡しで最も混乱しやすいポイントです。
4. C#の値渡しと参照渡しの違い
4-1. 値渡しと参照渡しの違いを表で比較
値渡しと参照渡しの違いを整理すると、次のようになります。
| 比較項目 | 値渡し | 参照渡し |
|---|---|---|
| 渡されるもの | 変数の中身のコピー | 変数そのものへの参照 |
| 呼び出し元の変数 | 基本的に変更されない | 変更できる |
| 通常の引数 | 該当する | 該当しない |
ref / out / in | 使わない | 使う |
| 参照型の場合 | 参照のコピーが渡される | 参照変数そのものを渡す |
| 読みやすさ | 基本的にわかりやすい | 使いすぎると追いにくい |
C#では、特別な理由がなければ値渡しを使うのが基本です。
参照渡しは、呼び出し元の変数を書き換えたい場合や、パフォーマンス上の理由がある場合に使います。
4-2. 参照渡しとは「変数そのものを渡す」こと
参照渡しとは、メソッドに変数のコピーではなく、呼び出し元の変数そのものを操作できる形で渡すことです。
C#では、ref、out、inを使うことで参照渡しを行えます。
C#static void Change(ref int x)
{
x = 100;
}
このようにrefを付けると、メソッド内のxは呼び出し元の変数と結びつきます。
C#int number = 10;
Change(ref number);
Console.WriteLine(number); // 100
通常の値渡しではnumberは変わりませんが、refを使うとnumberそのものが変更されます。
4-3. refを使うと呼び出し元の変数を変更できる
refを使った例を見てみましょう。
C#using System;
class Program
{
static void Main()
{
int value = 10;
Change(ref value);
Console.WriteLine(value); // 100
}
static void Change(ref int x)
{
x = 100;
}
}
実行結果は次のとおりです。
100
refを使う場合は、メソッド定義側と呼び出し側の両方にrefを付ける必要があります。
C#Change(ref value);
これにより、「このメソッドは呼び出し元の変数を変更する可能性がある」とコード上で明示できます。
4-4. 値渡しと参照渡しを使い分ける判断基準
基本的には、まず値渡しを使います。
値渡しは副作用が少なく、メソッドの動作を追いやすいからです。
参照渡しを検討するのは、次のような場合です。
値渡しを使うケース
- 呼び出し元の変数を変更する必要がない
- メソッドの入力として値を使うだけ
- シンプルで読みやすいコードにしたい
参照渡しを使うケース
- 呼び出し元の変数を書き換えたい
- 複数の結果を返したい
- 大きな構造体のコピーを避けたい
ただし、複数の結果を返したい場合でも、必ずoutを使う必要はありません。
最近のC#では、タプルや専用の戻り値クラスを使った方が読みやすいことも多いです。
4-5. 図解:値渡しはコピー、参照渡しは同じ変数を共有
値渡しのイメージは次のとおりです。
呼び出し元
+------------+
| value = 10 |
+------------+
|
| コピー
v
メソッド内
+------------+
| x = 10 |
+------------+
メソッド内でxを変更しても、呼び出し元のvalueは変わりません。
呼び出し元
+------------+
| value = 10 |
+------------+
メソッド内
+------------+
| x = 100 |
+------------+
一方、参照渡しでは、メソッド内の引数が呼び出し元の変数と結びつきます。
呼び出し元 / メソッド内
+----------------------+
| value / x = 10 |
+----------------------+
メソッド内でxを変更すると、呼び出し元のvalueも変わります。
呼び出し元 / メソッド内
+----------------------+
| value / x = 100 |
+----------------------+
値渡しはコピー、参照渡しは同じ変数を共有するイメージです。
5. ref・out・inの違いを整理
5-1. ref:呼び出し前に代入が必要で、値の読み書きができる
refは、呼び出し元の変数をメソッド内で読み書きしたいときに使います。
refを使う変数は、メソッドを呼び出す前に必ず値が代入されている必要があります。
C#static void Double(ref int x)
{
x *= 2;
}
呼び出し側は次のようになります。
C#int number = 10;
Double(ref number);
Console.WriteLine(number); // 20
refでは、メソッド内で引数の値を読むことも、書き換えることもできます。
refの特徴
- 呼び出し前に代入が必要
- メソッド内で値を読める
- メソッド内で値を書き換えられる
- 呼び出し元の変数に変更が反映される
5-2. out:メソッド内で必ず代入する戻り値用の引数
outは、メソッドから値を返すための引数として使います。
代表的な例は、int.TryParseです。
C#string text = "123";
bool success = int.TryParse(text, out int result);
Console.WriteLine(success); // True
Console.WriteLine(result); // 123
out引数は、メソッド内で必ず値を代入する必要があります。
C#static void GetValues(out int x, out int y)
{
x = 10;
y = 20;
}
呼び出し側は次のようになります。
C#GetValues(out int a, out int b);
Console.WriteLine(a); // 10
Console.WriteLine(b); // 20
outは、メソッドの戻り値とは別に追加の値を返したいときに使われます。
5-3. in:読み取り専用で渡す引数
inは、引数を読み取り専用として参照渡ししたいときに使います。
主に、大きな構造体のコピーを避けたい場合に使われます。
C#struct LargeData
{
public int A;
public int B;
public int C;
public int D;
}
static int Sum(in LargeData data)
{
return data.A + data.B + data.C + data.D;
}
inを使うと、メソッド内で引数を書き換えることはできません。
C#static void Update(in LargeData data)
{
// data.A = 100; // コンパイルエラー
}
inは、値を変更したくないが、コピーコストを避けたい場合に有効です。
ただし、小さな値型に対してむやみに使っても、読みやすさが下がるだけの場合があります。
5-4. ref・out・inの使い方をサンプルコードで比較
ref、out、inの違いをコードで比較してみましょう。
C#using System;
struct Point
{
public int X;
public int Y;
}
class Program
{
static void Main()
{
int value = 10;
ChangeRef(ref value);
Console.WriteLine(value); // 20
CreateOut(out int result);
Console.WriteLine(result); // 100
Point point = new Point { X = 3, Y = 4 };
int total = SumIn(in point);
Console.WriteLine(total); // 7
}
static void ChangeRef(ref int x)
{
x = 20;
}
static void CreateOut(out int x)
{
x = 100;
}
static int SumIn(in Point p)
{
return p.X + p.Y;
}
}
それぞれの使い方を整理すると、次のようになります。
| 修飾子 | 主な目的 | 呼び出し前の代入 | メソッド内での代入 | 呼び出し元への反映 |
|---|---|---|---|---|
| なし | 通常の値渡し | 必要 | 任意 | 基本的に反映されない |
ref | 値の読み書き | 必要 | 任意 | 反映される |
out | 値を返す | 不要 | 必須 | 反映される |
in | 読み取り専用で渡す | 必要 | 不可 | 書き換え不可 |
5-5. ref・out・inを使いすぎると読みにくくなる理由
ref、out、inは便利ですが、使いすぎるとコードが読みにくくなります。
特にrefやoutが多いメソッドは、どの引数がどこで変更されるのか追いにくくなります。
C#Update(ref a, ref b, out c, out d);
このようなコードは、呼び出し側を見ただけでは処理の意味がわかりにくくなりがちです。
複数の値を返したい場合は、次のようにタプルを使う方法もあります。
C#static (int min, int max) GetMinMax(int[] numbers)
{
int min = numbers.Min();
int max = numbers.Max();
return (min, max);
}
呼び出し側は次のように書けます。
C#var result = GetMinMax(numbers);
Console.WriteLine(result.min);
Console.WriteLine(result.max);
また、意味のあるデータ構造を返したい場合は、専用のクラスやレコードを作る方がわかりやすい場合もあります。
C#record RangeResult(int Min, int Max);
ref、out、inは必要な場面で使うべき機能ですが、まずは戻り値で表現できないかを考えると、読みやすい設計になりやすいです。
6. 値渡しでよくある誤解と間違い
6-1. 「参照型を渡す=参照渡し」ではない
C#で非常によくある誤解が、「参照型をメソッドに渡すと参照渡しになる」というものです。
これは正しくありません。
参照型を通常の引数として渡した場合も、引数そのものは値渡しです。
C#static void Method(Person p)
{
}
この場合、pにはオブジェクトへの参照がコピーされて渡されます。
つまり、正確には次のように表現できます。
参照型を値渡ししている
参照型の値渡しでは、参照のコピーが渡されるため、メソッド内から同じオブジェクトを操作できます。
しかし、引数そのものに別のオブジェクトを代入しても、呼び出し元の変数は変わりません。
6-2. Listを変更できるのは参照型の中身を操作しているから
List<T>をメソッドに渡すと、メソッド内で要素を追加できます。
C#using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
List<string> fruits = new List<string> { "Apple" };
AddFruit(fruits);
Console.WriteLine(string.Join(", ", fruits)); // Apple, Banana
}
static void AddFruit(List<string> list)
{
list.Add("Banana");
}
}
実行結果は次のとおりです。
Apple, Banana
これは、listとfruitsが同じListオブジェクトを指しているためです。
ただし、メソッド内でlistに新しいListを代入しても、呼び出し元のfruitsは変わりません。
C#static void ReplaceList(List<string> list)
{
list = new List<string> { "Orange" };
}
この場合、変更されるのはメソッド内のlistだけです。
呼び出し元のfruitsは、元のListを指したままです。
6-3. stringは参照型だが変更できないように見える理由
stringは参照型です。
しかし、stringをメソッドに渡して変更しても、呼び出し元の文字列は変わらないように見えます。
C#using System;
class Program
{
static void Main()
{
string name = "Alice";
ChangeString(name);
Console.WriteLine(name); // Alice
}
static void ChangeString(string s)
{
s = "Bob";
}
}
実行結果は次のようになります。
Alice
stringは参照型ですが、文字列オブジェクトは不変です。
つまり、一度作られた文字列の中身を変更することはできません。
C#s = "Bob";
このコードは、文字列オブジェクトの中身を変更しているのではなく、引数sが別の文字列オブジェクトを指すようにしているだけです。
nameとsは別の変数なので、sの参照先を変更しても、呼び出し元のnameには影響しません。
6-4. メソッド内で引数に代入しても元の変数が変わらない理由
値渡しでは、メソッド内の引数は呼び出し元の変数とは別物です。
そのため、メソッド内で引数に代入しても、元の変数は変わりません。
C#static void Change(int x)
{
x = 100;
}
この場合、変更されるのはxだけです。
C#static void Change(Person p)
{
p = new Person { Name = "Bob" };
}
参照型の場合でも同じです。
pに新しいオブジェクトを代入しても、変更されるのはメソッド内の引数pだけです。
呼び出し元の変数が指すオブジェクトを変更したい場合は、refを使う必要があります。
C#static void Change(ref Person p)
{
p = new Person { Name = "Bob" };
}
このようにすると、呼び出し元の変数の参照先も変更できます。
6-5. 図解:変わるケースと変わらないケースの違い
参照型の値渡しで、変わるケースと変わらないケースを整理します。
まず、オブジェクトの中身を変更するケースです。
C#p.Name = "Bob";
呼び出し元 ヒープ
+------------+ +----------------+
| person ----|----------> | Name = Alice |
+------------+ +----------------+
メソッド内
+------------+
| p ---------|----------> 同じオブジェクト
+------------+
p.Nameを変更すると、同じオブジェクトの中身が変わります。
呼び出し元 ヒープ
+------------+ +----------------+
| person ----|----------> | Name = Bob |
+------------+ +----------------+
メソッド内
+------------+
| p ---------|----------> 同じオブジェクト
+------------+
この場合、呼び出し元から見ても変更されています。
次に、引数そのものに新しいオブジェクトを代入するケースです。
C#p = new Person { Name = "Carol" };
呼び出し元 ヒープ
+------------+ +----------------+
| person ----|----------> | Name = Bob |
+------------+ +----------------+
メソッド内 ヒープ
+------------+ +----------------+
| p ---------|----------> | Name = Carol |
+------------+ +----------------+
この場合、pの参照先は変わりますが、呼び出し元のpersonは元のオブジェクトを指したままです。
つまり、次のように覚えるとわかりやすいです。
オブジェクトの中身を変える → 呼び出し元にも影響する
引数に別のオブジェクトを代入 → 呼び出し元の変数は変わらない
7. 値渡し・参照渡しの実践サンプル
7-1. 値型を値渡しする例
まずは、値型を値渡しする例です。
C#using System;
class Program
{
static void Main()
{
int score = 80;
AddBonus(score);
Console.WriteLine(score); // 80
}
static void AddBonus(int value)
{
value += 10;
Console.WriteLine(value); // 90
}
}
AddBonusメソッド内ではvalueが90になります。
しかし、呼び出し元のscoreは80のままです。
値型を通常の引数として渡すと、値そのものがコピーされるためです。
7-2. 参照型を値渡しする例
次に、参照型を値渡しする例です。
C#using System;
class User
{
public string Name;
}
class Program
{
static void Main()
{
User user = new User { Name = "Alice" };
Rename(user);
Console.WriteLine(user.Name); // Bob
}
static void Rename(User u)
{
u.Name = "Bob";
}
}
userとuは別の変数ですが、同じUserオブジェクトを指しています。
そのため、u.Nameを変更すると、呼び出し元のuser.Nameも変更されたように見えます。
ただし、次のようにuに新しいオブジェクトを代入しても、呼び出し元のuserは変わりません。
C#static void Replace(User u)
{
u = new User { Name = "Carol" };
}
これは、uが参照のコピーだからです。
7-3. refで呼び出し元の値を変更する例
refを使うと、呼び出し元の変数をメソッド内で変更できます。
C#using System;
class Program
{
static void Main()
{
int price = 1000;
ApplyDiscount(ref price);
Console.WriteLine(price); // 800
}
static void ApplyDiscount(ref int value)
{
value -= 200;
}
}
このコードでは、ApplyDiscountメソッド内でvalueを変更すると、呼び出し元のpriceも変更されます。
refを使う場合、呼び出し側にもrefを書く必要があります。
C#ApplyDiscount(ref price);
これにより、呼び出し元の変数が変更される可能性があることが明確になります。
7-4. outで複数の結果を受け取る例
outは、メソッドから複数の結果を返したいときによく使われます。
C#using System;
class Program
{
static void Main()
{
bool success = TryDivide(10, 2, out int result);
if (success)
{
Console.WriteLine(result); // 5
}
}
static bool TryDivide(int a, int b, out int result)
{
if (b == 0)
{
result = 0;
return false;
}
result = a / b;
return true;
}
}
この例では、割り算に成功したかどうかをboolで返し、計算結果をout引数で返しています。
out引数は、メソッド内で必ず代入する必要があります。
7-5. inで大きな構造体を読み取り専用で渡す例
inは、大きな構造体をコピーせず、読み取り専用で渡したいときに使います。
C#using System;
struct RectangleData
{
public double Width;
public double Height;
public double X;
public double Y;
}
class Program
{
static void Main()
{
RectangleData rect = new RectangleData
{
Width = 10,
Height = 5,
X = 0,
Y = 0
};
double area = CalculateArea(in rect);
Console.WriteLine(area); // 50
}
static double CalculateArea(in RectangleData rect)
{
return rect.Width * rect.Height;
}
}
inを使うことで、rectを読み取り専用として渡せます。
メソッド内で次のように変更しようとすると、コンパイルエラーになります。
C#// rect.Width = 20; // コンパイルエラー
inは、値を書き換えたくないことを明示しつつ、コピーコストを抑えたい場合に使います。
8. 値渡しを理解するためのメモリイメージ
8-1. スタックとヒープの基本イメージ
C#の値渡しを理解するうえで、スタックとヒープのイメージを知っておくと役立ちます。
厳密な内部実装をすべて覚える必要はありませんが、初心者のうちは次のように考えると理解しやすいです。
スタック
- ローカル変数やメソッド呼び出しに関する情報を置く場所
- 値型のデータや参照型の参照が置かれることが多い
ヒープ
- newで作られたオブジェクト本体が置かれる場所
- classや配列、Listなどの実体が置かれる
たとえば、次のコードを考えます。
C#int number = 10;
Person person = new Person();
イメージとしては、次のようになります。
スタック
+----------------+
| number = 10 |
+----------------+
| person --------|----+
+----------------+ |
v
ヒープ
+----------------+
| Person object |
+----------------+
値型は値そのもの、参照型はオブジェクトへの参照を持つ、と考えるとわかりやすいです。
8-2. 値型がメモリ上でどのように扱われるか
値型の変数には、値そのものが入っていると考えます。
C#int a = 10;
int b = a;
この場合、aの値がbにコピーされます。
+--------+
| a = 10 |
+--------+
+--------+
| b = 10 |
+--------+
その後、bを変更してもaは変わりません。
C#b = 20;
+--------+
| a = 10 |
+--------+
+--------+
| b = 20 |
+--------+
値型では、それぞれの変数が独立した値を持つと考えると理解しやすいです。
8-3. 参照型がメモリ上でどのように扱われるか
参照型の変数には、オブジェクトそのものではなく、オブジェクトへの参照が入っています。
C#Person a = new Person { Name = "Alice" };
Person b = a;
この場合、aとbは同じオブジェクトを指します。
スタック ヒープ
+--------+ +----------------+
| a -----|------------> | Name = Alice |
+--------+ +----------------+
| b -----|------------> 同じオブジェクト
+--------+
そのため、b.Nameを変更すると、a.Nameも変わったように見えます。
C#b.Name = "Bob";
スタック ヒープ
+--------+ +----------------+
| a -----|------------> | Name = Bob |
+--------+ +----------------+
| b -----|------------> 同じオブジェクト
+--------+
ただし、bに新しいオブジェクトを代入すると、bだけが別のオブジェクトを指すようになります。
C#b = new Person { Name = "Carol" };
スタック ヒープ
+--------+ +----------------+
| a -----|------------> | Name = Bob |
+--------+ +----------------+
+--------+ +----------------+
| b -----|------------> | Name = Carol |
+--------+ +----------------+
8-4. 値渡しと参照渡しをメモリ図で比較
値型を値渡しする場合のイメージです。
C#int a = 10;
Change(a);
呼び出し元
+--------+
| a = 10 |
+--------+
メソッド内
+--------+
| x = 10 |
+--------+
xはaのコピーなので、xを変更してもaは変わりません。
一方、refを使った参照渡しでは、メソッド内の引数が呼び出し元の変数と結びつきます。
C#int a = 10;
Change(ref a);
呼び出し元の変数をメソッド内から直接操作するイメージ
+----------------+
| a / x = 10 |
+----------------+
x = 100;とすると、呼び出し元のaも100になります。
参照型の値渡しでは、参照のコピーが渡されます。
C#Person person = new Person { Name = "Alice" };
ChangeName(person);
呼び出し元 ヒープ
+------------+ +----------------+
| person ----|----------> | Name = Alice |
+------------+ +----------------+
メソッド内
+------------+
| p ---------|----------> 同じオブジェクト
+------------+
このため、オブジェクトの中身は変更できますが、呼び出し元の変数そのものを別のオブジェクトに差し替えることはできません。
8-5. 初心者はまず「変数のコピー」と「参照先の共有」で理解する
メモリの細かい仕組みを最初から完全に理解する必要はありません。
初心者のうちは、次の2つを押さえれば十分です。
値型の値渡し
→ 値そのものがコピーされる
参照型の値渡し
→ オブジェクトへの参照がコピーされる
そして、参照型では次の違いを意識しましょう。
オブジェクトの中身を変更する
→ 呼び出し元にも影響する
引数に別のオブジェクトを代入する
→ 呼び出し元の変数は変わらない
この考え方が身につくと、List、配列、class、stringをメソッドに渡したときの挙動を正しく理解できるようになります。
9. 値渡し・参照渡しの使い分け方
9-1. 基本は値渡しを使う
C#では、基本的に値渡しを使います。
通常のメソッド引数は値渡しなので、特別な理由がなければそのまま書けば問題ありません。
C#static int Add(int a, int b)
{
return a + b;
}
このように、入力を受け取り、結果を戻り値で返すメソッドは、シンプルで読みやすいです。
値渡しは、呼び出し元の変数を意図せず変更しにくいため、安全で扱いやすい書き方です。
9-2. 呼び出し元の変数を書き換えたい場合はrefを検討する
呼び出し元の変数そのものを書き換えたい場合は、refを検討します。
C#static void Swap(ref int a, ref int b)
{
int temp = a;
a = b;
b = temp;
}
呼び出し側は次のようになります。
C#int x = 10;
int y = 20;
Swap(ref x, ref y);
Console.WriteLine(x); // 20
Console.WriteLine(y); // 10
Swapのように、呼び出し元の変数を直接入れ替える必要がある場合は、refが適しています。
ただし、refは副作用が強くなるため、使いすぎには注意が必要です。
9-3. 複数の値を返したい場合はoutより戻り値やタプルも検討する
複数の値を返したい場合、outを使う方法があります。
C#static bool TryGetUser(out string name, out int age)
{
name = "Alice";
age = 30;
return true;
}
ただし、常にoutが最適とは限りません。
タプルを使うと、次のように書けます。
C#static (string Name, int Age) GetUser()
{
return ("Alice", 30);
}
呼び出し側は次のようになります。
C#var user = GetUser();
Console.WriteLine(user.Name);
Console.WriteLine(user.Age);
さらに、意味のあるデータとして扱いたい場合は、専用の型を作るのもよい方法です。
C#record UserResult(string Name, int Age);
static UserResult GetUser()
{
return new UserResult("Alice", 30);
}
outは、TryParseのように「成功したかどうか」と「結果」を分けて返すパターンでは便利です。
一方で、複数の値を自然なデータとして扱いたい場合は、タプルや専用型の方が読みやすくなります。
9-4. パフォーマンス目的でinを使うケース
inは、主に大きな構造体のコピーを避けたい場合に使います。
通常、小さなintやboolに対してinを使う必要はほとんどありません。
C#static int Add(in int a, in int b)
{
return a + b;
}
このようなコードは、かえって読みづらくなる場合があります。
inが向いているのは、次のようなケースです。
- 大きなstructを渡す
- メソッド内で変更する必要がない
- コピーコストを避けたい
- 読み取り専用であることを明示したい
ただし、パフォーマンス目的でinを使う場合は、実際に効果があるか測定することが大切です。
9-5. 可読性を下げないための設計ポイント
値渡し・参照渡しを使い分けるときは、可読性を重視することが重要です。
特に、refやoutが多いメソッドは注意が必要です。
C#Calculate(ref a, ref b, out c, out d, out e);
このようなメソッドは、呼び出し側から見て何が起こるのか分かりにくくなります。
読みやすい設計にするには、次のような方針を意識するとよいです。
- 基本は値渡しにする
- 結果は戻り値で返す
- 複数の値はタプルや専用型を検討する
- refやoutは必要な場面に限定する
- 引数を変更するメソッドは名前で意図を明確にする
たとえば、呼び出し元の値を変更するメソッドなら、Update、Reset、Swapなど、変更があることを想像しやすい名前にするとよいでしょう。
10. C#の値渡しに関するよくある質問
10-1. C#は値渡しと参照渡しのどちらが基本ですか?
C#のメソッド引数は、基本的に値渡しです。
C#static void Method(int value)
{
}
このように通常の引数として渡した場合、呼び出し元の変数の中身がコピーされます。
参照渡しをしたい場合は、ref、out、inを明示的に使います。
C#static void Method(ref int value)
{
}
つまり、C#では「通常は値渡し、必要なときだけ参照渡し」と考えるとよいです。
10-2. 参照型を渡すと参照渡しになりますか?
いいえ、参照型を通常の引数として渡しても、参照渡しにはなりません。
参照型の場合は、オブジェクトへの参照が値渡しされます。
C#static void Method(Person person)
{
}
この場合、personには参照のコピーが渡されます。
そのため、メソッド内でオブジェクトの中身を変更すると呼び出し元にも影響します。
しかし、メソッド内で引数に新しいオブジェクトを代入しても、呼び出し元の変数は変わりません。
C#static void Method(Person person)
{
person = new Person();
}
呼び出し元の変数そのものを変更したい場合は、refを使う必要があります。
10-3. refとoutの違いは何ですか?
refとoutはどちらも呼び出し元の変数に影響を与える引数ですが、目的が異なります。
| 修飾子 | 目的 | 呼び出し前の代入 | メソッド内での代入 |
|---|---|---|---|
ref | 既存の値を読み書きする | 必要 | 任意 |
out | メソッドから値を返す | 不要 | 必須 |
refは、メソッド内で元の値を読んだうえで変更したい場合に使います。
C#static void AddTen(ref int value)
{
value += 10;
}
outは、メソッドの結果として値を設定したい場合に使います。
C#static void CreateValue(out int value)
{
value = 100;
}
TryParseのようなパターンでは、outがよく使われます。
10-4. 配列やListをメソッドに渡すと元のデータは変わりますか?
配列やList<T>は参照型です。
そのため、通常の引数として渡した場合、参照のコピーが渡されます。
メソッド内で要素を変更した場合、呼び出し元にも影響します。
C#static void ChangeArray(int[] numbers)
{
numbers[0] = 100;
}
C#int[] values = { 1, 2, 3 };
ChangeArray(values);
Console.WriteLine(values[0]); // 100
List<T>でも同じです。
C#static void AddItem(List<string> items)
{
items.Add("New");
}
このように、オブジェクトの中身を変更すると、呼び出し元から見えるデータも変わります。
ただし、メソッド内で引数に新しい配列や新しいListを代入しても、呼び出し元の変数は変わりません。
C#static void ReplaceList(List<string> items)
{
items = new List<string>();
}
10-5. stringを渡した場合はどうなりますか?
stringは参照型ですが、不変の型です。
そのため、文字列そのものの中身を変更することはできません。
C#static void Change(string text)
{
text = "Changed";
}
このコードでは、textが新しい文字列を指すようになるだけです。
呼び出し元の変数は変わりません。
C#string message = "Hello";
Change(message);
Console.WriteLine(message); // Hello
stringは参照型ですが、通常の引数として渡す場合は参照のコピーが渡されます。
さらに文字列は不変なので、List<T>のように中身を変更する動きにはなりません。
呼び出し元の文字列変数そのものを変更したい場合は、refを使います。
C#static void Change(ref string text)
{
text = "Changed";
}
C#string message = "Hello";
Change(ref message);
Console.WriteLine(message); // Changed
まとめ
C#の値渡しとは、メソッドに引数を渡すときに、変数の中身をコピーして渡す仕組みです。
C#の通常のメソッド引数は、基本的に値渡しです。
C#static void Method(int value)
{
}
値型の場合は、値そのものがコピーされます。
intやboolやstruct
→ データそのものがコピーされる
→ メソッド内で変更しても呼び出し元は変わらない
参照型の場合は、オブジェクトへの参照がコピーされます。
classや配列やList
→ 参照がコピーされる
→ 同じオブジェクトの中身は変更できる
→ 引数にnewを代入しても呼び出し元の変数は変わらない
ここで重要なのは、「参照型を渡すこと」と「参照渡し」は別物だという点です。
参照型を通常の引数として渡しても、それは参照渡しではなく、参照の値渡しです。
呼び出し元の変数そのものを変更したい場合は、refを使います。
C#static void Change(ref int value)
{
value = 100;
}
複数の値を返したい場合は、outを使えます。
C#static bool TryGetValue(out int value)
{
value = 10;
return true;
}
大きな構造体を読み取り専用で渡したい場合は、inを使うことがあります。
C#static int Calculate(in LargeStruct data)
{
return data.Value;
}
ただし、ref、out、inを使いすぎると、コードの流れが追いにくくなることがあります。
基本は値渡しを使い、必要な場面だけ参照渡しを使うのが、C#では読みやすく安全な書き方です。
最後に、C#の値渡しを理解するためのポイントをもう一度整理します。
通常の引数は値渡し
値型は値そのものがコピーされる
参照型は参照がコピーされる
参照型の中身を変更すると呼び出し元にも影響する
引数に再代入しても呼び出し元の変数は変わらない
呼び出し元の変数を変更したい場合はrefを使う
この考え方を押さえておけば、C#の値渡し、参照渡し、ref、out、inの違いで混乱しにくくなります。

