C# 言語のツアー

C# ("シー シャープ" と読みます) は、最新のタイプ セーフなオブジェクト指向のプログラミング言語です。 開発者は C# を使用することにより、.NET で稼働する、安全かつ堅牢な多くの種類のアプリケーションを構築できます。 C# は C 言語ファミリーをルーツとしているため、C、C++、Java、JavaScript のプログラマーであればすぐに使いこなすことができます。 このツアーでは、C# 8 以前の言語の主要なコンポーネントの概要について説明します。 対話型の例を通して言語を調べたい場合は、C# の概要に関するチュートリアルを参照してください。

C# は、オブジェクト指向で "コンポーネント指向" のプログラミング言語です。 C# はこれらの概念を直接サポートする言語コンストラクトを提供しているので、自然にソフトウェア コンポーネントを作成して使用することができます。 当初から、C# では、新しいワークロードと新しいソフトウェア設計プラクティスをサポートする機能が追加されています。 根本的に、C# は "オブジェクト指向" 言語です。 ユーザーが型とその動作を定義します。

C# には、堅牢で持続性のあるアプリケーションを作成するのに役立つ機能がいくつかあります。 "ガベージ コレクション" には、到達できず、使用されていないオブジェクトによって占有されたメモリを自動的に回収する機能があります。 "null 許容型" を使用すると、割り当てられたオブジェクトを参照していない変数に対する保護が行われます。 "例外処理" には、エラーの検出と復旧を行うための構造化された拡張可能なアプローチが用意されています。 "ラムダ式" では、関数型プログラミング手法がサポートされます。 "統合言語クエリ (LINQ)" の構文を使用すると、任意のソースからのデータを操作するための一般的なパターンが作成されます。 "非同期操作" の言語サポートからは、分散システムを構築するための構文が提供されます。 C# は "統合型システム" を備えています。 intdouble などのプリミティブ型を含めた C# のすべての型は、ルートとなる 1 つの object 型から派生しています。 すべての型は、一般的な操作のセットを共有します。 すべての型の値を一貫した方法で格納、転送、操作することができます。 さらに、C# を使用すると、ユーザー定義の参照型値型の両方がサポートされます。 C# では、オブジェクトを動的に割り当てたり、軽量の構造体をインラインで格納したりすることもできます。 C# は、タイプ セーフとパフォーマンスの向上を実現するジェネリック メソッドと型をサポートします。 C# は反復子もサポートしているため、クライアント コードのカスタム動作をコレクション クラスの実装側が定義できます。

C# では、プログラムとライブラリが互換性を保ちながら時間とともに進化できるように、"バージョン管理" に重点が置かれています。 C# の設計でバージョン管理の考慮の影響を直接受けている側面として、別個の virtual 修飾子と override 修飾子、メソッドのオーバーロードの解決規則、明示的なインターフェイス メンバー宣言のサポートなどがあります。

.NET のアーキテクチャ

C# プログラムは、.NET 上で実行されます。これは、共通言語ランタイム (CLR) と呼ばれる仮想実行システムであり、クラス ライブラリのセットです。 CLR は、国際規格である共通言語基盤 (CLI) の Microsoft による実装です。 CLI は、言語やライブラリをシームレスに連携する実行および開発環境を構築するための基盤です。

C# で記述したソース コードは、CLI 仕様に準拠する中間言語 (IL) にコンパイルされます。 IL コードおよびリソース (ビットマップや文字列など) は、アセンブリに保存されます。拡張子は一般的に .dll です。 アセンブリに含まれるマニフェストには、アセンブリの種類、バージョン、およびカルチャに関する情報が規定されています。

C# プログラムを実行すると、アセンブリが CLR に読み込まれます。 CLR によって Just-In-Time (JIT) コンパイルが実行され、IL コードはネイティブのマシン語命令に変換されます。 CLR には、自動的なガベージ コレクション、例外処理、およびリソース管理に関する他のサービスが用意されています。 CLR で実行されるコードは、"マネージド コード" と呼ばれることがあります。"アンマネージド コード" は、特定のプラットフォームを対象とするネイティブ マシン言語にコンパイルされます。

言語の相互運用性は、.NET の主要機能です。 C# コンパイラによって生成された IL コードは、共通型の仕様 (CTS) に準拠しています。 C# から生成された IL コードは、F#、Visual Basic、C++ の .NET バージョンから生成されたコードと相互運用性があります。 他にも 20 を超える CTS 準拠言語があります。 単一のアセンブリに、異なる .NET 言語で記述された複数のモジュールが含まれる場合があります。 型は同じ言語で記述されているかのように相互参照することができます。

.NET には、実行時のサービス以外にも、広範なライブラリが用意されています。 これらのライブラリは、さまざまなワークロードをサポートします。 これらは、さまざまな便利な機能を提供する名前空間に編成されています。 ライブラリには、ファイル入出力から、文字列操作、XML 解析、Web アプリケーション フレームワーク、Windows フォーム コントロールまですべてが含まれています。 C# アプリケーションでは、一般に、.NET クラス ライブラリを広範囲に使用して、一般的な "配管工事" のような作業を処理しています。

.NET の詳細については、.NET の概要に関する記事を参照してください。

Hello world

"Hello, World" は、プログラミング言語を紹介するために伝統的に使用されているプログラムです。 これを C# で記述すると次のようになります。

using System;

class Hello
{
    static void Main()
    {
        Console.WriteLine("Hello, World");
    }
}

"Hello, World" プログラムは System 名前空間を参照する using ディレクティブで始まります。 名前空間は、C# のプログラムとライブラリを階層的に整理するための手段です。 名前空間には、型と他の名前空間が含まれます。たとえば、System 名前空間には多数の型 (プログラムで参照される Console クラスなど) と、他の多数の名前空間 (IOCollections など) が含まれます。 特定の名前空間を参照する using ディレクティブを使用すると、その名前空間のメンバーである型を修飾せずに使用できます。 using ディレクティブにより、プログラムで Console.WriteLineSystem.Console.WriteLine の省略形として使用できます。

"Hello, World" プログラムで宣言された Hello クラスにはメンバーが 1 つあります。Main という名前のメソッドです。 Main メソッドは static 修飾子を使用して宣言されています。 インスタンス メソッドが this で囲んだ特定のオブジェクト インスタンスを参照できるのに対し、静的メソッドは特定のオブジェクトを参照せずに機能します。 規則により、Main という名前の静的メソッドは C# プログラムのエントリ ポイントとして使用されます。

プログラムの出力は、System 名前空間にある Console クラスの WriteLine メソッドによって生成されます。 このクラスは、コンパイラによって自動的に参照される、標準のクラス ライブラリで提供されています。

型と変数

"" によって、C# のあらゆるデータの構造と動作が定義されます。 型の宣言には、そのメンバー、基本データ型、実装するインターフェイス、その型に対して許可される操作を含めることができます。 "変数" は、特定の型のインスタンスを参照するラベルです。

C# には、値型参照型という 2 種類の型があります。 値の型の変数には、データが直接格納されます。 参照型の変数には、データへの参照が格納され、これはオブジェクトとして知られています。 参照型を使用すると 2 つの変数が同じオブジェクトを参照でき、1 つの変数に対する演算によって、もう一方の変数によって参照されるオブジェクトに影響を与えることができます。 値型の場合、各変数によって独自のデータ コピーが保持され、1 つの変数に対する演算によって別の変数に影響を与えることはできません (refout のパラメーターの変数を除く)。

"識別子" は変数名です。 識別子は、空白を含まない unicode 文字のシーケンスです。 先頭に @ が指定されている場合、識別子は C# の予約語になります。 予約語を識別子として使用すると、他の言語と対話するときに役立つ場合があります。

C# の値の型はさらに、"単純型"、"列挙型"、"構造体型"、"null 許容値型"、"タプル値型" に分けられます。 C# の参照型はさらに、"クラス型"、"インターフェイス型"、"配列型"、および "デリゲート型" に分けられます。

以下は、C# の型システムの概要です。

C# プログラムでは型宣言を使用して新しい型を作成します。 型宣言は、新しい型の名前とメンバーを指定します。 C# の型カテゴリのうち 6 つはユーザー定義が可能です。それは、クラス型、構造体型、インターフェイス型、列挙型、デリゲート型、タプル値型です。 record 型を record struct または record class のどちらかで宣言することもできます。 レコード型には、コンパイラによって合成されたメンバーが含まれます。 レコードは主に値の格納に使用しますが、関連付けられている動作は最小限です。

  • class 型は、データ メンバー (フィールド) と関数メンバー (メソッド、プロパティ、その他) を含むデータ構造を定義します。 クラス型では、単一継承とポリモーフィズムをサポートします。このメカニズムによって派生クラスが基底クラスを拡張して特殊化できます。
  • struct 型は、データ メンバーおよび関数メンバーで構造体を表す点において、クラス型に似ています。 ただしクラスと異なり、構造体は値型で、通常はヒープ割り当てが不要です。 構造体型ではユーザー指定の継承がサポートされず、すべての構造体型によって暗黙的に object 型が継承されます。
  • interface 型では、パブリック メンバーの名前付きセットとしてコントラクトが定義されます。 interface を実装する class または struct は、インターフェイスのメンバーの実装を提供する必要があります。 interface は複数の基底インターフェイスから継承することがあり、class または struct は複数のインターフェイスを実装することがあります。
  • delegate 型は、特定のパラメーター リストおよび戻り値を使用してメソッドへの参照を表します。 デリゲートを使用すると、変数に割り当ててパラメーターとして渡すことのできるエンティティとして、メソッドを処理できます。 デリゲートは、関数型言語で提供される関数の型に似ています。 また、他のいくつかの言語で見られる関数ポインターの概念に似ています。 ただし、関数ポインターとは異なり、デリゲートはオブジェクト指向で、タイプ セーフです。

classstructinterface および delegate の型はすべてジェネリックをサポートし、他の型と共にパラメーター化できます。

C# では、あらゆる型の 1 次元および多次元の配列がサポートされています。 上記の型とは異なり、配列型は使用前に宣言する必要がありません。 代わりに配列型は、角かっこで囲んだ型名を後に付けることにより構成されます。 たとえば、int[]int の 1 次元配列、int[,]int の 2 次元配列、int[][]int の 1 次元配列の 1 次元配列、または "ジャグ" 配列です。

null 許容型は個別の定義を必要としません。 null 非許容型 T のそれぞれについて、対応する null 許容型 T? があり、これは追加値 null を保持することができます。 たとえば、int? は 32 ビット整数または値 null を保持できる型であり、string?string または値 null を保持できる型です。

C# の型システムは、任意の型の値を object として扱えるように統一されています。 C# における型はすべて、直接的または間接的に object クラス型から派生し、object はすべての型の究極の基底クラスです。 参照型の値は、値を単純に object 型としてみなすことによってオブジェクトとして扱われます。 値型の値は、ボックス化ボックス化解除操作を実行することによって、オブジェクトとして扱われます。 次の例では、int 値は object 値に変換され、また int に戻されます。

int i = 123;
object o = i;    // Boxing
int j = (int)o;  // Unboxing

値型の値が object 参照に割り当てられている場合は、値を保持するために "ボックス" が割り当てられます。 このボックスは参照型のインスタンスであり、そのボックスに値がコピーされます。 逆に、object 参照が値型にキャストされると、参照先の object が適切な値型のボックスかどうかが確認されます。 確認が成功すると、ボックス内の値が値型にコピーされます。

C# の型システムが統一されたということは、実質的には値型が "オンデマンドで" object 参照として扱われるということです。こうした統一性があるため、object 型を使用する汎用的なライブラリは、参照型と値型の両方を含め、object から派生するすべての型で使用できます。

C# には、フィールド、配列要素、ローカル変数、パラメーターなどの、いくつかの種類の変数があります。 変数は格納場所を表します。 各変数には型があり、次のように、この型によって変数に格納できる値が決まります。

  • null 非許容値型
    • 型そのものの値
  • null 許容値型
    • null 値、またはその型そのものの値
  • object
    • null 参照、任意の参照型のオブジェクトへの参照、または任意の値型のボックス化された値への参照
  • クラス型
    • null 参照、そのクラス型のインスタンスへの参照、またはそのクラス型から派生したクラスのインスタンスへの参照
  • インターフェイスの型
    • null 参照、そのインターフェイスの型を実装するクラス型のインスタンスへの参照、またはそのインターフェイス型を実装する値型のボックス化された値への参照
  • 配列型
    • null 参照、その配列型のインスタンスへの参照、または互換性のある配列型のインスタンスへの参照
  • デリゲート型
    • null 参照、またはそのデリゲート型と互換性のあるインスタンスへの参照

プログラムの構造

C# における主要な組織的概念は、"プログラム"、"名前空間"、""、"メンバー"、および "アセンブリ" です。 プログラムは型を宣言します。型にはメンバーが含まれていて、複数の名前空間に編成することができます。 型の例には、クラス、構造体、インターフェイスがあります。 メンバーの例には、フィールド、メソッド、プロパティ、およびイベントがあります。 C# プログラムはコンパイルされると、物理的にアセンブリにパッケージ化されます。 通常、アセンブリには、".exe" と "ライブラリ" のどちらを実装するかに応じて、それぞれ .exe または .dll のファイル拡張子が付けられます。

小さな例として、次のコードを含むアセンブリについて考えてみます。

namespace Acme.Collections;

public class Stack<T>
{
    Entry _top;

    public void Push(T data)
    {
        _top = new Entry(_top, data);
    }

    public T Pop()
    {
        if (_top == null)
        {
            throw new InvalidOperationException();
        }
        T result = _top.Data;
        _top = _top.Next;

        return result;
    }

    class Entry
    {
        public Entry Next { get; set; }
        public T Data { get; set; }

        public Entry(Entry next, T data)
        {
            Next = next;
            Data = data;
        }
    }
}

このクラスの完全修飾名は Acme.Collections.Stack です。 このクラスには複数のメンバーが含まれています: _top という名前のフィールドが 1 つ、PushPop という名前のメソッドが合わせて 2 つ、そして Entry という名前の入れ子になったクラスです。 さらに、Entry クラスには、Next という名前のプロパティ、Data という名前のプロパティ、およびコンストラクターの 3 つのメンバーが含まれています。 Stack は "Stack" クラスです。 これには、使用時に具象型に置き換えられる 1 つの型パラメーター T があります。

"スタック" は "先入れ後出し" (FILO) コレクションです。 新しい要素がスタックの先頭に追加されます。 要素が削除されると、それはスタックの一番上から削除されます。 前の例では、スタックの格納と動作を定義する Stack 型が宣言されています。 Stack 型のインスタンスを参照する変数を宣言して、その機能を使用できます。

アセンブリには実行可能なコードが中間言語 (IL) の形式で含まれていて、シンボル情報がメタデータの形式で含まれています。 実行前に、.NET 共通言語ランタイムの Just-In-Time (JIT) コンパイラによって、アセンブリの IL コードはプロセッサ固有のコードに変換されます。

アセンブリはコードとメタデータの両方を含む自己記述的な機能的単位であるため、#include ディレクティブおよびヘッダー ファイルを C# に含める必要はありません。 特定のアセンブリに含まれているパブリックの型とメンバーは、単にプログラムのコンパイル中にそのアセンブリを参照することにより、C# プログラムで利用可能になります。 たとえば、このプログラムでは acme.dll アセンブリの Acme.Collections.Stack クラスを使用しています。

class Example
{
    public static void Main()
    {
        var s = new Acme.Collections.Stack<int>();
        s.Push(1); // stack contains 1
        s.Push(10); // stack contains 1, 10
        s.Push(100); // stack contains 1, 10, 100
        Console.WriteLine(s.Pop()); // stack contains 1, 10
        Console.WriteLine(s.Pop()); // stack contains 1
        Console.WriteLine(s.Pop()); // stack is empty
    }
}

このプログラムをコンパイルするには、前の例で定義されているスタック クラスを含むアセンブリを "参照" する必要があります。

C# プログラムは複数のソース ファイルに格納できます。 C# プログラムがコンパイルされると、すべてのソース ファイルがまとめて処理され、ソース ファイルは自由に相互参照できます。 概念的には、処理される前にすべてのソース ファイルが 1 つの大きなファイルに連結されるかのようになります。 C# では事前宣言をする必要がありません。ごく一部の例外を除いて、宣言の順序は重要でないためです。 C# ではソース ファイルがパブリック型 1 つのみの宣言に制限されません。また、ソース ファイルの名前がソース ファイルで宣言された型に一致する必要もありません。

これらの組織ブロックについては、このツアーの他の記事をご覧ください。