まえがき
最近までGoを書いていた私がC#に触れ始めました。今回は「型・変数・定数」まわりについて理解したことを、Goとの比較を踏まえながら記事にしていこうと思います!
また比較といっていますが両言語は思想が異なるため、優劣をつける意図はありません。用途と前提が違うだけという視点でフラットに見ていきます。
型
値型と参照型
C# は静的型付けで、型は大きく 値型 と 参照型 に分かれます。
-
値型:(例:
struct,int,bool,DateTimeなど)- “値そのもの”を持つイメージ
-
参照型:(例:
class,string,object,配列など)- 値の“参照(アドレス)”を持つイメージ
Goとの比較
Go も静的型付けですが、値型が基本になります。もし関数の引数に変数の参照を渡したいと思った場合は、ポインターを使用します。
後に解説するスライスなどは参照型みたいに思われがちですが、こちらも内部的にはポインターが使われているため値型といえます。
func main() {
var x int = 10
// &xにすることで、変数xの参照を引数として渡している
increment(&x)
// 参照元の値が変わっている
fmt.Println(x) // 11
}
// intのポインターを引数として受け取る関数
func increment(p *int) {
*p++
}
比較ポイント
- C#では大きく「値型」「参照型」 に分かれる。
- Goは「値型」が基本だが、「参照型」にするならポインターと組み合わせる。
クラス/構造体/レコード
C#のクラス(class)/構造体(struct)/レコード(record)は、用途に応じて使い分けるデータ型です。
組み合わせによっても機能が変わったりもするので以下で整理して記載します。
クラス(class)
- 型: 参照型
- 継承: できる
- 等価性: メモリ内の同じオブジェクトを参照していれば等価
- 用途: オブジェクト指向(振る舞い)の定義
構造体(struct)
- 型: 値型
- 継承: できない
- 等価性: 型が同じで同じメンバ値が格納されていれば等価
- 用途: 小さく軽いデータ(座標、識別子、日時など)と相性が良い
レコード(record class / record struct)
- レコード型には、
record classとrecord structがある。(recordだけを使う書き方もあるがrecord classと同じ意味) -
型:
-
record classは参照型 -
record structは値型
-
-
継承:
-
record classはできる -
record structはできない
-
- 等価性: 型が同じで同じメンバ値が格納されていれば等価
-
用途:
-
record classは、DTO/設定/レスポンス等データ中心 -
record structは、値として扱うDTO/小さめデータ(住所、バリューオブジェクトなど)
-
// クラス
public class Person
{
public required string LastName { get; set; }
public required string FirstName { get; set; }
}
// レコード
public record struct Point
{
public double X { get; init; }
public double Y { get; init; }
public double Z { get; init; }
}
// 構造体
public struct Coords
{
public double X { get; }
public double Y { get; }
}
Goとの比較
Goには構造体(struct)というものがあります。
Go の struct は 値型として振る舞うのが基本となっておりますが、ポインターを使用(*struct)することで参照型のように振る舞うことも可能です。
またGoには継承という概念がなく、構造体に型を埋め込む(embedding) ことで埋め込んだ型の実装を借りることができます。
// 構造体(person)
type person struct {
Name string
}
// 構造体(person)はgreet関数を持つ
func (p person) greet() string {
return fmt.Sprintf("Hello, %s", p.Name)
}
// 構造体(citizen)
type citizen struct {
person // 構造体(person)を埋め込み
Country string
}
func main() {
c := citizen{
Country: "japan",
}
// 構造体(person)が埋め込まれているためNameのフィールドを持つ
c.Name = "hoge"
// 構造体(person)が埋め込まれているためgreet関数を持つ
greeting := c.greet()
fmt.Println(greeting)
}
比較ポイント
- C# は 参照型/値型 + クラス(class)/構造体(struct)/レコード(record)定義 で「表現力」が高い
- Go は ポインター + struct + 埋め込み(embedding) の「組み合わせ」が強い。
配列とリスト
C#には、固定長の配列(T[N])と可変長のリスト(List<T>)があります。
int[] array = new int[5];
array[0] = 1;
array[1] = 2;
List<int> list = new List<>();
list.Add(1); // 追加
list.Add(2);
Goとの比較
Goには、固定長の配列([N]T)と可変長のスライス([]T)があります。
var arr [4]int
arr[0] = 1
arr[1] = 2
s := make([]int)
// または
s := []int{}
s = append(s, 1) // 追加
s = append(s, 2)
比較ポイント
- C# は固定長の配列(
T[N])と可変長のリスト(List<T>)がある - Go は固定長の配列(
[N]T)と可変長のスライス([]T)がある
代表的な型の比較(全体)
これまでの内容を踏まえて、プリミティブ型を含む型をまとめると以下になります。
| 目的 | C# | Go | メモ |
|---|---|---|---|
| 整数(符号付き) |
sbyte / short / int / long
|
int8 / int16 / int32 / int64
|
C#はサイズが明確、Goにはintもあるが環境依存(32/64) |
| 整数(符号なし) |
byte / ushort / uint / ulong
|
uint8(byte) / uint16 / uint32 / uint64
|
C#はサイズが明確、Goにはuintもあるが環境依存(32/64) |
| 浮動小数 |
float / double
|
float32 / float64
|
|
| 高精度小数 | decimal |
なし ※float64は丸め誤差を含んでしまう | |
| 真偽 | bool |
bool |
|
| 文字 | char |
rune |
|
| 文字列 | string |
string |
C#のstringは参照型、Goは値型(ポインターで参照型) |
| 時刻 |
DateTime / DateTimeOffset
|
time.Time |
|
| 配列/可変長 |
T[N] / List<T>
|
[N]T / []T(スライス) |
|
| 構造体/オブジェクト |
class/ struct/ record
|
struct |
C#は表現が多い、Goはポインターなどとの組み合わせで表現 |
変数
変数定義
C# は「型を書く」か「型推論(var)」が基本です。
- 明示的に型を書く
-
varで型推論 - ローカル変数は 代入してから使用(未初期化のまま使えない)
// 明示的に型を指定
int count = 10;
string name = "gopher";
// 型推論
var total = 123; // int
var message = "hello"; // string
// 宣言だけして後で代入(初期化していないとコンパイルエラーになる)
int x;
x = 5;
Goとの比較
Go は var と := で書くのが基本になります。
:=はvarの省略形で、使用頻度はかなり高いです。
C#との大きな違いとしては、Goにはゼロ値という概念があり、変数は代入しなくても初期化されるという点です。
例えば、int型の場合は0で、string型の場合は""で初期化されます。
ゼロ値についてもっと詳しく(サンプルコードあり)
Go はすべての型に ゼロ値があります。var で宣言するとゼロ値で初期化されます。
- 数値:
0 -
bool:false -
string:""(空文字) - ポインタ / slice / map / chan / func / interface:
nil - struct: 各フィールドがゼロ値(いわゆる“ゼロ値struct”)
type User struct {
Name string
Age int
}
func main() {
var n int
var ok bool
var s string
var p *User
var sl []int
var m map[string]int
var u User
fmt.Println(n) // 0
fmt.Println(ok) // false
fmt.Println(s) // ""
fmt.Println(p == nil) // true
fmt.Println(sl == nil)// true
fmt.Println(m == nil) // true
fmt.Printf("%+v\n", u) // {Name: Age:0}
}
// 明示的に型を指定
var count int = 10
// 型推論
var name = "gopher"
// 短縮宣言(よく使う)
total := 123
message := "hello"
// 宣言だけしても代入せずに使える(0(ゼロ値)が初期値となっている)
var x int
比較ポイント
- どちらにも型推論がある(C#:
var/ Go::=やvar) - C#は代入していない変数は使えずコンパイル時にエラーになる、Go は変数を宣言時にゼロ値で初期化される
定数
const と readonly
C# は「定数」を宣言する方法が2種類あるのが特徴です。
-
const- コンパイル時に値が決定
- 整数型や文字列型 など、扱える型に制限がある
-
readonly- 実行時に値が決定する“実質定数”
- フィールドに付ける(
static readonlyもある) - 扱える型に制限はない
public class Config
{
// コンパイル時に決定
public const int MaxRetry = 3;
// 実行時に決定
public static readonly DateTime StartedAt = DateTime.UtcNow;
// インスタンス生成時に決定
public readonly string Name;
public Config(string name)
{
Name = name; // コンストラクタ内で(インスタンス生成時に)代入
}
}
Goとの比較
Go の定数にするかの判断はシンプルで、定数にできるならconst、無理なら var で定義します。
// 定数
const MaxRetry = 3
// 実行時に決まるものはconstにできないのでvar
var StartedAt = time.Now().UTC()
比較ポイント
- C#は、値が決定するタイミングで
constとreadonlyで使い分ける - Goは、定数は
constそれ以外はvarでシンプルに使い分ける
まとめ
- C#の型は「値型/参照型」がある。Goは「値型」が基本
- C#は
class/struct/recordなどで型を表現しやすい。Goはstructやポインターの組み合わせで表現。 -
変数定義は、C#は
var、Goはvar/:=で定義できる。どちらも型推論がある。Goの変数にはゼロ値があり変数の宣言をするだけで初期値が設定される。 -
定数定義は、C#は値が決定するタイミングで
constとreadonlyで使い分ける、Goは基本constでそれ以外はvar。
今回は「型・変数・定数」まわりを記事にしてみました。
Goは比較的シンプルなのに対して、C#はどう表現するかによって扱いが変わってくるので今後のプロジェクトでの注意しながら開発していこうと思います!