Qiitaでも探せばありそうなネタです。
はじめに
最近C#の学習を始めたのですが、ちょっと面白いなと思ったので投稿してみたいと思います。
散々QAされてきた内容なので「ああ、あれか」と思う方も多いと思いますが、自分も調べた内容をまとめてみます。
さて、今回のネタ、初めて学ぶプログラミング言語がC#の場合は何も疑問に思うことはないと思います。
他の言語を学んだこともある方も、ただC#を学ぶだけでしたら何も面白さは分からないと思います。
では、何が面白いのか。
ここではintをベースに紹介したいと思います。
いきなりまとめ
結論が知りたい方もいると思いますので。とりあえず要点をまとめます。
- C#のintは言語仕様的にSystem.Int32のエイリアス
- System.Int32の実装(Int32.cs)を見てみるとフィールド定義にintが使われている
- 通常C#では構造体の再帰(循環)定義はコンパイルエラーになる
- さらにC#のコンパイラー(Roslyn)はC#で実装されている(いわゆるセルフホスティング)
- それでも再帰定義しているSystem.Int32は不思議!Int32.csはどうやってコンパイルしているのか!
この何となく不思議な点を調べてみると、コンパイラー(Roslyn)さんがエラーにしないようにしているからなんだよ、ってお話です。
C#におけるintとは
マイクロソフトが公開しているC# 言語ツアーを読み始めると、他の言語(特に多くの社畜プログラマーが学んでるであろうJava)を学んだことのある方は気になる文章があります。
以下の文章は、C# 言語ツアーからたどれるキーワードからの引用です(詳細はこちら)。
単純型はキーワードで識別されますが、これらのキーワードは、System 名前空間に事前に定義されている構造体型の単なるエイリアスです。 たとえば、int は System.Int32 のエイリアスです。 エイリアスの完全な一覧については、「組み込み型の一覧表」を参照してください。
ね、気になりますよね?
説明を読む限り、C#におけるintは構造体型System.Int32のエイリアス(別名)にすぎません。
Java言語で例えるなら、intはjava.lang.Integerのエイリアス、と言うことです。
(あくまで例えであり、実際のJavaでは違います)
さて、言語的にSystem.Int32は構造体であると言うことは、structキーワードを用いた定義があるはずです。(組み込み型なんだし、struct定義は無くてもいいんじゃね?って思う人もいるでしょうけど……)
実際にSystem.Int32の定義は存在しているので、ソースコードを見てみましょう。
System.Int32の定義
構造体System.Int32はどの様に定義されているのでしょうか。
(ソースコードを公開するなんて、昔のマイクロソフトでは考えられないですよね……)
.Net Coreのソースコードを見てみると、次のように定義されています。
namespace System
{
[Serializable]
[StructLayout(LayoutKind.Sequential)]
[TypeForwardedFrom("mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")]
public readonly struct Int32 : IComparable, IConvertible, IFormattable, IComparable<int>, IEquatable<int>, ISpanFormattable
{
private readonly int m_value; // Do not rename (binary serialization)
// 以下、省略
.Net Frameworkのソースコードは.Net Coreと異なりますが、以下のような定義になっています(少し加工しています)。
namespace System {
[Serializable]
[System.Runtime.InteropServices.StructLayout(LayoutKind.Sequential)]
[System.Runtime.InteropServices.ComVisible(true)]
public struct Int32 : IComparable, IFormattable, IConvertible
{
internal int m_value;
// 以下、省略
この実装を覚えておいてください。
構造体の再帰定義
※再帰、循環、自己言及、自己参照、どの言葉が適切何だろうか……とりあえずrecursive(再帰)として記事を書いていきます。
先ほど引用したソースコード、何か気になるポイントありますよね。もちろん、namespaceの括弧の位置でははありません。
この部分です。
private readonly int m_value;
internal int m_value;
m_valueの宣言の仕方は異なるものの、どちらもintで定義しています。
もうお気づきですよね。
そうです。intはSystem.Int32のエイリアスにも関わらず、System.Int32の定義にintを用いています。
これは言語仕様的に
private readonly System.Int32 m_value;
と書き換えられるはずです。
実際に、次の構造体Tは
public struct T
{
int foo;
}
次のように
public struct T
{
System.Int32 foo;
}
と書き換えても、アセンブリ(メタ情報とILコード)は同じになります。(デバッグ情報とか含めると話は違ってくると思いますが、その辺は割愛)
一方、少し学習をすると分かりますが、C#では(ユーザーによる)構造体の再帰定義はエラーになります。つまり、次のような構造体型はコンパイルエラーになります。
public struct T
{
T foo;
}
以下はちょっと自信のない説明なので、指摘いただければ幸いです
そもそも、なぜ構造体の再帰定義がエラーになるのでしょうか。
乱暴に言えば「変数自体にデータを保持するために、どれだけメモリーを確保したら分からない」からです。
C#における構造体は値型で、値型の変数は変数自体に値が保持されています。
例えば値を表現するために64ビット(8バイト)必要な場合、値型変数のためにはメモリーを64ビット分確保する必要があります。
では上記構造体Tを型とする変数は何ビット必要でしょうか。
構造体Tを実装した人も想像つかないでしょう。何ビット必要とするか全くヒントがありません。
話は変わりますが参照型の場合、例え変数の参照先が数十メガのデータサイズのオブジェクトであっても、変数自体が保持するのは参照(メモリーのアドレス)なので32ビットや64ビットと決めることができます。
従って、参照型の場合は次のような再帰定義が許容されます。
public class T
{
T foo;
}
では、なぜ値型である構造体System.Int32の再帰定義は許容されているのでしょうか。
System.Int32の再帰定義が許容される理由
先ほど構造体の再帰定義がエラーとなる原因を「変数自体にデータを保持するためにどれだけメモリーを確保したら分からない」と記載しました。
これは、逆に考えると「変数自体にどれだけメモリーを確保したら良いか分かっていれば、コンパイルエラーにならない」とも言えるのではないでしょうか。
ここまで書くとなんとなく分かりますよね。
そうです。C#におけるSystem.Int32型変数は「変数自体にどれだけメモリーを確保したら良いか分かっている」からコンパイルエラーにならないのです。
それはなぜでしょうか。
答えは、構造体System.Int32はC#の組み込み型だからです。
従って、System.Int32が再帰定義であってもコンパイルエラーにする必要は無いのです。
(実際のところ、コンパイラー自身がメモリーを何ビット必要か知っている必要はなく、コンパイラーが出力したアセンブリで実行環境が変数用にメモリーを確保できるかが問題なんだと思います……)
では、コンパイラーの実装はどの様になっているのでしょうか。
C#のコンパイラーであるRoslynの構造体の再帰定義をエラーとする実装を見てみましょう。
詳細はこちらをご覧ください。以下に、一部抜粋します。
private bool HasStructCircularity(DiagnosticBag diagnostics)
{
foreach (var valuesByName in GetMembersByName().Values)
{
foreach (var member in valuesByName)
{
if (member.Kind != SymbolKind.Field)
{
// NOTE: don't have to check field-like events, because they can't have struct types.
continue;
}
var field = (FieldSymbol)member;
if (field.IsStatic)
{
continue;
}
var type = field.NonPointerType();
if (((object)type != null) &&
(type.TypeKind == TypeKind.Struct) &&
BaseTypeAnalysis.StructDependsOn((NamedTypeSymbol)type, this) &&
!type.IsPrimitiveRecursiveStruct()) // allow System.Int32 to contain a field of its own type
{
// If this is a backing field, report the error on the associated property.
var symbol = field.AssociatedSymbol ?? field;
if (symbol.Kind == SymbolKind.Parameter)
{
// We should stick to members for this error.
symbol = field;
}
// Struct member '{0}' of type '{1}' causes a cycle in the struct layout
diagnostics.Add(ErrorCode.ERR_StructLayoutCycle, symbol.Locations[0], symbol, type);
return true;
}
}
}
return false;
}
再帰判定しているのは1876行目付近から定義されているHasStructCircularityメソッドなのですが、1896行目に気になる条件式があります。
!type.IsPrimitiveRecursiveStruct()) // allow System.Int32 to contain a field of its own type
コメントにもありますが、そのまんまですね。
プリミティブ型(組み込み型)の再帰定義の構造体は許容するような判定メソッドがあります。
IsPrimitiveRecursiveStructメソッドはSpecialTypeExtensionsクラスに定義されています。詳細はこちら
メモリー云々の細かい話はともかく、コンパイラーレベルでエラーとしないようになっていたことが分かりました!
まとめ、再び
長々と書きましたが、コンパイラーのソースまで読んでみるとSystem.Int32含め組み込み型は特別扱いされており、再帰定義しても問題ないことが分かりました。