本文へスキップ
ウェブエンジニア問題集
第3章

C#の変数と型 — 値型・参照型とvarの使い分け

約9分
この章の目次開く

C#は静的型付けの言語で、すべての変数に型があります。TypeScriptと似た書き味ですが、決定的に違うのは型が実行時にも残ることです。

この章では、変数の宣言方法と組み込み型を確認したうえで、C#でつまずきやすい「値型と参照型」の違いを整理します。

変数を宣言する

C#の変数宣言は「型 変数名 = 初期値」の順で書きます。

int count = 10;
string name = "田中";
bool isActive = true;
double price = 1980.5;
csharp

右辺から型が明らかな場合は、var で型推論に任せられます。

var count = 10;          // int と推論される
var name = "田中";       // string と推論される
var users = new List<string>();  // List<string> と推論される
csharp

変更しない値には const を使います。宣言時に値が確定している必要があります。

const double TaxRate = 0.1;
csharp

よく使う組み込み型

型何を入れるか例
int整数(約 -21億〜21億)int age = 30;
longより大きい整数long total = 10_000_000_000;
double小数(高速だが誤差あり)double ratio = 0.75;
decimal小数(誤差が出にくい)decimal price = 1980.00m;
bool真偽値bool isDone = false;
char1文字char grade = 'A';
string文字列string name = "田中";
DateTime日時var now = DateTime.Now;

数値リテラルは 1_000_000 のようにアンダースコアで桁を区切れます。decimal には m、float には f のサフィックスが必要です。

値型と参照型

C#の型は、大きく値型と参照型に分かれます。ここはC#の挙動を理解するうえで最重要のポイントです。

値型参照型
代表例int / double / bool / struct / enumstring / class / 配列 / List<T>
変数が持つもの値そのもの実体への参照(アドレス)
代入したとき値がコピーされる参照がコピーされる(実体は共有)
既定値0 / false などnull

違いはコードで見ると一目瞭然です。

// 値型:コピーされる
int a = 5;
int b = a;
b = 10;
Console.WriteLine(a);  // 5(影響を受けない)
 
// 参照型:同じ実体を指す
var list1 = new List<int> { 1, 2, 3 };
var list2 = list1;
list2.Add(4);
Console.WriteLine(list1.Count);  // 4(list1も変わる)
csharp
代入やメソッドへの受け渡しで「コピーされるのか、同じ実体を共有するのか」は、値型か参照型かで決まります。
データの受け渡しのイメージ
値型は「値の写し」を渡し、参照型は「置き場所の案内」を渡します
学習者学習者

JavaScriptでもオブジェクトを渡すと元が変わりますよね。あれと同じですか?

考え方は同じです。JavaScriptでプリミティブとオブジェクトで挙動が違うのと同様に、C#では値型と参照型で挙動が変わります。違いは、C#では struct を定義することで自分で値型を作れる点です。

stringは参照型だが、値のように振る舞う

string は参照型ですが、不変(イミュータブル)です。文字列を変更する操作は、元の文字列を書き換えるのではなく、新しい文字列を作ります。

string s1 = "abc";
string s2 = s1;
s2 += "d";
Console.WriteLine(s1);  // abc(変わらない)
csharp

比較も、参照ではなく中身で行われます。

string a = "hello";
string b = "hel" + "lo";
Console.WriteLine(a == b);  // True
csharp

文字列の書き方

文字列に変数を埋め込むときは、先頭に $ を付けた文字列補間を使います。

string name = "田中";
int age = 30;
Console.WriteLine($"{name}さんは{age}歳です");
csharp

パスや正規表現のようにバックスラッシュを多用する場合は、先頭に @ を付けると改行やエスケープをそのまま書けます。

string path = @"C:\Users\tanaka\Documents";
csharp

複数行のJSONやSQLを埋め込むときは、C# 11以降で使える生文字列リテラルが便利です。

string json = """
    { "name": "田中", "age": 30 }
    """;
csharp

型を変換する

型変換には、失敗しうるものと、しないものがあります。

方法例失敗したとき
暗黙の変換long total = intValue;起きない(情報が失われない方向のみ)
明示的キャストint n = (int)doubleValue;小数部が切り捨てられる
Parseint.Parse("42")例外が発生する
TryParseint.TryParse(input, out var n)false を返す(例外は出ない)
ConvertConvert.ToInt32("42")例外が発生する

ユーザー入力や外部データを数値にするときは、TryParse を使うのが基本です。

構文: int.TryParse(s, out result)

引数渡せるもの説明
s(第1引数)文字列 / null変換したい文字列
result(第2引数)out 付きの int 変数変換に成功した場合、値がここに入る

戻り値: 変換できたかどうかの bool(失敗時は result に 0 が入る)

if (int.TryParse(input, out var age))
{
    Console.WriteLine($"{age}歳ですね");
}
else
{
    Console.WriteLine("数値を入力してください");
}
csharp

out var age は「ここで変数を宣言して、結果を受け取る」という書き方です。out については C#のメソッド で扱います。

structとenum

自分で値型を定義したいときは struct、決まった選択肢を表したいときは enum を使います。

enum OrderStatus
{
    Pending,
    Shipped,
    Delivered
}
 
var status = OrderStatus.Shipped;
csharp

enum は「文字列で状態を持ち回って打ち間違える」事故を防げます。TypeScriptのユニオン型('pending' | 'shipped')に近い役割です。

struct は小さなデータをまとめて値型として扱いたいときに使いますが、実務では基本的に class か record を選び、必要になってから struct を検討すれば十分です。

よくあるハマりどころ

整数同士の割り算で小数が消える

int a = 7;
int b = 2;
Console.WriteLine(a / b);           // 3(切り捨て)
Console.WriteLine((double)a / b);   // 3.5
csharp

int 同士の除算は結果も int になります。小数がほしい場合は、どちらかを double にキャストします。

参照型を渡したのに「コピーされたと思っていた」

メソッドにリストを渡して中身を変更すると、呼び出し元のリストも変わります。変更したくない場合は、新しいリストを作って渡します。

var を使いすぎて型が読めない

var users = GetUsers(); のように、右辺を見ても型が分からない書き方は避けます。目安は「右辺に型名が現れているなら var、そうでなければ型を明記」です。

先生先生

var user = new User(); はOK。var result = Calc(); は型を書いたほうが親切、という感覚だね。

ちゃんと使うためのポイント

  • 変数は「型 名前 = 値」。右辺から型が明らかなら var を使う
  • 金額や税計算は double ではなく decimal
  • 値型は代入でコピー、参照型は実体を共有する
  • string は参照型だが不変で、== は中身の比較になる
  • 文字列の組み立ては文字列補間($"...")
  • 外部からの入力を数値にするときは Parse ではなく TryParse

次の章では、条件分岐と繰り返しに進みます。C#では switch が式として書けるようになっており、ここがJavaScriptとの書き味の差が出るところです。

参考リンク