C#構文の概要

C#構文の概要

C#10は. NET 6とVS2022でリリースされました。この記事では、. NETのリリース順に、Microsoftの公式ドキュメントに基づいてC#の興味深い構文機能を整理します。

最后更新 2021/11/19 17:38
louzixl
预计阅读 15 分钟
分类
.NET
标签
.NET C#

C# 10 已与 .NET 6、VS2022 一起发布,本文按照.NET 的发布顺序,根据微软官方文档整理 C#中一些有趣的语法特性。

注:基于不同.NET 平台创建的项目,默认支持的 C#版本是不一样的。下面介绍的语法特性,会说明引入 C#的版本,在使用过程中,需要注意使用 C#的版本是否支持对应的特性。C#语言版本控制,可参考官方文档

匿名関数です

匿名関数はC#2で導入された関数で、名前はなくメソッド本体を持ちます。匿名関数はdelegateで作成され、デリゲートに変換できます。匿名関数は戻り値の型を指定する必要はありません。return文に基づいて自動的に戻り値の型を決定します。

** 注意:C#3以降にラムダ式が導入され、ラムダを使うと匿名関数をより簡潔に作成できます。ラムダとは異なり、delegateを使用して匿名関数を作成すると、引数リストを省略し、任意の引数リストを持つデリゲート型に変換できます。

// 使用delegate关键字创建,无需指定返回值,可转换为委托,可省略参数列表(与lambda不同)
Func<int, bool> func = delegate { return true; };

自動属性

C#3以降では、プロパティアクセサに追加のロジックが必要ない場合に自動プロパティを使用して、より簡潔な方法でプロパティを宣言できるようになった。コンパイル時に、コンパイラーはgetおよびsetアクセサからのみアクセスできるプライベートで匿名のフィールドを作成します。VSを使用して開発する場合、snippetコードスニペットprop+2回tabで自動属性を素早く生成することができます。

// 属性老写法
private string _name;
public string Name
{
    get { return _name; }
    set { _name = value; }
}
// 自动属性
public string Name { get; set; }

また、C#6以降では、自動属性を初期化できます。

public string Name { get; set; } = "Louzi";

匿名の種類

匿名型はC#3以降に導入された機能であり、定義型を表示する必要なく、読み取り専用属性のセットを単一のオブジェクトにカプセル化する。コンパイラーは自動的に匿名型の各プロパティの型を推定し、型名を生成します。CLRの観点からは、匿名型はオブジェクトから直接派生した他の参照型と変わらない。2つ以上の匿名オブジェクトが同じ順序、名前、型のプロパティを指定した場合、コンパイラーはそれらを同じ型のインスタンスとして扱います。匿名型を作成する際に、メンバ名を指定しない場合、コンパイラーはプロパティの初期化に使用した名前をプロパティ名として使用します。

匿名型はLINQクエリのselectクエリ式でよく使われます。匿名型はnewと初期化リストを使用して作成されます:

// 使用new与初始化列表创建匿名类型
var person = new { Name = "Louzi", Age = 18 };
Console.WriteLine($"Name: {person.Name}, Age: {person.Age}");
// 用于LINQ
var productQuery =
    from prod in products
    select new { prod.Color, prod.Price };
foreach (var v in productQuery)
{
    Console.WriteLine("Color={0}, Price={1}", v.Color, v.Price);
}

LINQ

C#3ではキラー機能、クエリ式、つまり言語統合クエリLINQが導入された。クエリ式はクエリ構文でクエリを表し、SQLのような構文で記述された一連の節で構成されます。

クエリ式はfrom節で始まり、select節またはgroup節で終わる必要があります。最初のfrom節と最後のselectまたはgroup節の間には、w here、orderby、join、let、その他のfrom節などを含めることができます。

LINQクエリは、SQLデータベース、XMLドキュメント、ADO.NETデータセット、IEnumerableまたはIEnumerableインターフェイスを実装したコレクションオブジェクトに対して実行できます。

完全なクエリは、データソースの作成、クエリ式の定義、クエリの実行を含みます。クエリ式変数は、クエリ結果ではなくストアクエリであり、クエリ変数をループした後にのみクエリが実行されます。

クエリ構文を使用して表現できる任意のクエリは、メソッドを使用して表現できます。より読みやすいクエリ構文を推奨します。CountやMaxなどのクエリ操作には、同等のクエリ式句がなく、メソッド呼び出しを使用する必要があります。メソッド呼び出しとクエリ構文を組み合わせて使用できます。

关于 LINQ 的详细文档,参见微软官方文档

// Data source.
int[] scores = { 90, 71, 82, 93, 75, 82 };
// Query Expression.
IEnumerable<int> scoreQuery = //query variable
    from score in scores //required
    where score > 80 // optional
    orderby score descending // optional
    select score; //must end with select or group
// Execute the query to produce the results
foreach (int testScore in scoreQuery)
{
    Console.WriteLine(testScore);
}

Lambda

C#3には自動プロパティ、拡張メソッド、暗黙型付け、LINQ、ラムダ式などの強力な機能が導入されている。

ラムダ式を作成するには、=>の左側に入力パラメータを指定し(空の括弧は0個の引数を指定し、1つの引数は括弧を省略することができる)、右側に式または文のブロック(通常2つまたは3つの文)を指定する。任意のラムダ式はデリゲート型に変換でき、式ラムダ文は式ツリーに変換できます(ラムダ文はできません)。

無名関数は引数リストを省略でき、Lambdaで使用されない引数は破棄(C#9)で指定できる。

asyncとawaitを使用して、非同期処理を含むラムダ式とステートメントを作成できます(C#5)。

C#10以降、コンパイラーが戻り値の型を推論できない場合、ラムダ式の戻り値の型を引数の前に指定することができます。

// Lambda转换为委托
Func<int, int> square = x => x * x;
// Lambda转换为表达式树
System.Linq.Expressions.Expression<Func<int, int>> e = x => x * x;
// 使用弃元指定不使用的参数
Func<int, int, int> constant = (_, _) => 42;
// 异步Lambda
var lambdaAsync = async () => await JustDelayAsync();
Console.WriteLine($"main thread id: {Thread.CurrentThread.ManagedThreadId}");
lambdaAsync();
static async Task JustDelayAsync()
{
    await Task.Delay(1000);
    Console.WriteLine($"JustDelayAsync thread id: {Thread.CurrentThread.ManagedThreadId}");
}
// 指定返回类型,不指定返回类型会报错
var choose = object (bool b) => b ? 1 : "two";

拡張方法論

拡張メソッドはC#3で導入された機能であり、元の型を変更することなく既存の型にメソッドを追加できる。extendメソッドは静的メソッドですが、インスタンスオブジェクト構文を介して呼び出され、最初の引数はメソッド操作の型を指定し、thisで修飾します。コンパイラはILにコンパイルするときに静的メソッドの呼び出しに変换します

拡張メソッドと同じ名前とシグネチャを持つ型のメソッドがある場合、コンパイラーは型のメソッドを選択します。コンパイラーがメソッド呼び出しを行うと、その型のインスタンスメソッドを探し、その型の拡張メソッドを検索することはできません。

最も一般的な拡張方法はLINQで、既存のSystem. Collections.IEnumerable型とSystem. Collections.Generic.IEnumerable型にクエリ機能を追加します。

structに拡張メソッドを追加する場合、値渡しのため、structオブジェクトのコピーに対してのみ変更できます。C#7.2以降、参照渡しのために最初の引数にref修飾を追加できるようになり、structオブジェクト自体を変更できるようになりました。

static class MyExtensions
{
    public static void OutputStringExtension(this string s) => Console.WriteLine($"output: {s}");
    public static void OutputPointExtension(this Point p)
    {
        p.X = 10;
        p.Y = 10;
        Console.WriteLine($"output: ({p.X}, {p.Y})");
    }
    public static void OutputPointWithRefExtension(ref this Point p)
    {
        p.X = 20;
        p.Y = 20;
        Console.WriteLine($"output: ({p.X}, {p.Y})");
    }
}
// class扩展方法
"Louzi".OutputStringExtension();
// struct扩展方法
Point p = new Point(5, 5);
p.OutputPointExtension(); // output: (10, 10)
Console.WriteLine($"original point: ({p.X}, {p.Y})");  // output: (5, 5)
p.OutputPointWithRefExtension();  // output: (20, 20)
Console.WriteLine($"original point: ({p.X}, {p.Y})");  // output: (20, 20)

暗黙型var

C#3以降、メソッドスコープ内で暗黙の型変数varを宣言できるようになりました。暗黙型は強型であり、型はコンパイラによって決定される。

varはコンストラクタを呼び出してオブジェクトインスタンスを作成するときによく使われますが、C#9以降、このシナリオでは型を決定するnew式も使用できます。

// 隐式类型
var s = new List<int>();
// new表达式
List<int> ss = new();

注:匿名型を返す場合はvarのみを使用できます。

オブジェクト、コレクションの初期化リスト

C#3以降では、オブジェクトやコレクションをインスタンス化し、メンバの割り当てを1つの文で実行できます。

オブジェクト初期化リストを使用すると、オブジェクトの作成時にオブジェクトのアクセス可能な任意のフィールドまたはプロパティに値を割り当てることができます。コンストラクタ引数を指定するか、引数を無視するか、括弧を使用できます。

public class Person
{
    // 自动属性
    public int Age { get; set; }
    public string Name { get; set; }
    public Person() { }
    public Person(string name)
    {
        Name = name;
    }
}
var p1 = new Person { Age = 18, Name = "Louzi" };
var p2 = new Person("Sherilyn") { Age = 18 };

C#6以降、オブジェクト初期化リストは、アクセス可能なフィールドとプロパティを初期化するだけでなく、インデクサも設定することができる。

public class MyIntArray
{
    public int CurrentIndex { get; set; }
    public int[] data = new int[3];
    public int this[int index]
    {
        get => data[index];
        set => data[index] = value;
    }
}
var myArray = new MyIntArray { [0] = 1, [1] = 3, [2] = 5, CurrentIndex = 0 };

コレクション初期化リストは、1つ以上の初期値を指定できます。

var persons = new List<Person>
{
    new Person { Age = 18, Name = "Louzi" },
    new Person { Age = 18, Name = "Sherilyn" }
};

組み込みジェネリックデリゲート

NET Framework 3.5/4.0では、それぞれActionとFuncのジェネリックデリゲート型が組み込まれている。void戻り値型のデリゲートはAction型を使用でき、Actionのバリアントは最大16個の引数を持ちます。戻り値の型を持つデリゲートはFunc型を使用でき、Func型のバリアントは同じ16個の引数を持ち、戻り値の型はFunc宣言の最後の型パラメータである。

Action<int> actionInstance = ActionInstance;
Func<int, string> funcInstance = FuncInstance;
static void ActionInstance(int n) => Console.WriteLine($"input: {n}");
static string FuncInstance(int n) => $"param: {n}";

dynamic

C#4の主な機能は動的キーワードの導入である。動的型は、変数とそのメンバー参照が使用されるときにコンパイル時型チェックをバイパスし、実行時に解決されます。これはJava Scriptのような動的型付け言語に似た構造を実装している。

dynamic dyn = 1;
Console.WriteLine(dyn.GetType()); // output: System.Int32
dyn = dyn + 3; // 如果dyn是object类型,此句则会报错

名前付きパラメータとオプションパラメータ

C#4では、名前付きパラメータとオプションパラメータが導入された。名前付きパラメータは、パラメータリスト内の位置に一致する必要なく、一致する実参加型パラメータを指定することで、形式パラメータの実引数を指定できます。オプションパラメータパラメータパラメータのデフォルト値を指定することで、引数を省略できます。オプションパラメータはパラメータリストの最後に配置する必要があります。一連のオプションパラメータのいずれかに引数が与えられた場合は、その引数の前にあるすべてのオプションパラメータに引数が与えられなければなりません。

OptionalAttributeプロパティを使用してオプションのパラメータを宣言することもできます。この場合、形式パラメータにデフォルト値を指定する必要はありません。

// 命名参数与可选参数
PrintPerson(age: 18, name: "Louzi");
// static void PrintPerson(string name, int age, [Optional, DefaultParameterValue("男")] string sex)
static void PrintPerson(string name, int age, string sex = "男") =>
    Console.WriteLine($"name: {name}, age: {age}, sex: {sex}");

静的インポート

C#6では、using staticディレクティブを使って型をインポートし、型名を指定せずに静的メンバやネストされた型にアクセスできるようになり、型名の重複入力によるあいまいなコードを回避できる。

using static System.Console;
WriteLine("Hello CSharp");

例外フィルタwhen

C#6以降では、catch文でwhenが使用できるようになり、特定の例外ハンドラを実行するためにtrueでなければならない条件式を指定し、式がfalseの場合は例外処理を実行しない。

public static async Task<string> MakeRequest()
{
    var client = new HttpClient();
    var streamTask = client.GetStringAsync("https://localHost:10000");
    try
    {
        var responseText = await streamTask;
        return responseText;
    }
    catch (HttpRequestException e) when (e.Message.Contains("301"))
    {
        return "Site Moved";
    }
    catch (HttpRequestException e) when (e.Message.Contains("404"))
    {
        return "Page Not Found";
    }
    catch (HttpRequestException e)
    {
        return e.Message;
    }
}

自動プロパティ初期化式{{じどうぷろぱてぃしょうかしき}}

C#6以降では、型デフォルト以外の値を使用する自動属性の初期化値を指定できます。

public class DefaultValueOfProperty
{
    public string MyProperty { get; set; } = "Property";
}

エクスプレッション·ボディ

C#6以降ではメソッド、演算子、読み取り専用プロパティの式ボディ定義がサポートされ、C#7.0以降ではコンストラクタ、ターミネーター、プロパティ、インデクサの式ボディ定義がサポートされています。

static void NewLine() => Console.WriteLine();

null 条件演算子

C#6から、null 条件演算子が導入されました。null 条件演算子は、オペランドの評価結果がnull以外の場合にのみ、メンバを.または要素アクセスEssbase []演算は、そのオペランドに適用されます。それ以外の場合はnullが返されます。

// null条件表达式
public class ConditionalNull
{
    event EventHandler AEvent;
    public void RaiseAEvent() => AEvent?.Invoke(this, EventArgs.Empty);
}

文字列の補間

C#6以降、\(を使って文字列に式を挿入できるようになり、コードを読みやすくし、文字列の連結エラーの可能性を減らしました。内挿文字列に中括弧を含むは、2つの中括弧({{“or”}})を使用します。補間式に条件演算子が必要な場合は、括弧で囲まれます。C#8以降では、\)@''または@\(''形式の補間単語文字列を使用できるようになり、それ以前のバージョンでは、\)@''形式を使用する必要があった。

Console.WriteLine($"{name} is {age} year{(age == 1 ? "" : "s")} old.");

nameof

C#6では、文字列定数として変数、型、またはメンバー名(完全修飾ではない)を生成するnameof式が提供されている。

public string Name
{
    get => name;
    set => name = value ?? throw new ArgumentNullException(nameof(value), $"{nameof(Name)} cannot be null");
}

改善点の外

C#7.0ではout構文が改良され、メソッド呼び出しのパラメータリスト内で直接out変数を宣言できるようになり、宣言文を別々に記述する必要はなくなった。

void Function(out int arg) { ... }
// 未改进前
int n;
Function(out n);
// 改进后
Function(out int n);

タプル

C#7.0で導入されたタプルの言語サポート(以前のバージョンではタプルもあったが非効率であった)では、タプルを使って複数のデータを含む単純な構造を表現でき、クラスや構造体を特別に記述する必要はなかった。タプルは値型であり、データメンバを表すために複数の共通フィールドを含む軽量データ構造であり、メソッドを定義することはできません。C#7.3以降、タプルは==と!=をサポートする。

// 方式一,使用元组字段的默认名称:Item1、Item2、Item3等
(string, string) unnamedLetters = ("a", "b");
Console.WriteLine($"{unnamedLetters.Item1}, {unnamedLetters.Item2}");
// 方式二
(string Alpha, string Beta) namedLetters = ("a", "b");
Console.WriteLine($"{namedLetters.Alpha}, {namedLetters.Beta}");
// 方式三
var alphabetStart = (Alpha: "a", Beta: "b");
Console.WriteLine($"{alphabetStart.Alpha}, {alphabetStart.Beta}");
// 方式四,C# 7.1开始支持自动推断变量名称
int count = 5;
string label = "Colors used in the map";
var pair = (count, label); // 元组元素名为"count"和"label"

メソッドがタプルを返すとき、タプルのメンバを抽出するには、タプルの各値に対して別々の変数を宣言することで実現できます。メソッド戻り値の型としてタプルを使用することは、outメソッドパラメータを定義する代わりにできます。

// 解构元组
var (first, last) = Range(numbers);
Console.WriteLine($"{first} to {last}");
(int max, int min) = Range(numbers);
Console.WriteLine($"{min} to {max}");

放棄されたドル

C#7.0以降、破棄元はプレースホルダ変数であり、代入されていない変数に相当し、その変数を使用したくないことを示し、アンダースコア_を使用して破棄元変数を示す。以下は、放棄された使用シナリオの一部です。

// 场景一:丢弃元组值
(_, _, area) = city.GetCityInformation(cityName);
// 场景二:从C# 9开始,可以丢弃Lambda表达式中的参数
Func<int, int, int> constant = (_, _) => 42;
// 场景三,丢弃out参数
DiscardsOut(out _);
static void DiscardsOut(out string s)
{
    s = "nothing";
    Console.WriteLine($"input is {s}");
}

パターンマッチングパターンマッチ

C#7.0ではパターンマッチング機能が追加され、その後の主要なC#リリースで拡張された。パターンマッチングは、式が特定の特性を持つかどうかをテストするために使用されます。is式、switch文、switch式はすべてパターンマッチングをサポートし、whenキーワードを使用してパターンの追加ルールを指定できます。

パターンマッチングには、宣言型、型、定数型、リレーショナル型、論理型、プロパティ型、位置型、var型、破棄型などの型が含まれています。

isモード式はis演算子機能を改善し、1つの命令で結果を割り当てることができます。

// is模式匹配
if (input is int count) do somthing... ;
// 老写法
if (input is int)
{
    int count = (int)input;
    do somthing... ;
}
// is模式进行空检查
string? message = "This is not the null string";
if (message is not null) Console.WriteLine(message);

defaultテキスト式

デフォルト値式生成型のデフォルト値、以前のバージョンではデフォルト演算子のみをサポートしていましたが、C#7.1以降はデフォルト式の機能が拡張され、コンパイラーが式の型を推論できる場合にデフォルトを使用して型のデフォルト値を生成できます。

// 新写法
Func<string, bool> whereClause = default;
// 老写法
Func<string, bool> whereClause = default(Func<string, bool>);

switch式#switch式#switchしき#

C#8以降、switch式を使用できるようになりました。switch文と比較してswitch式の改善点は次のとおりです。

  • switchキーワードの前の変数;
  • =>での置換caseの置換構造体こうぞう;
  • default演算子をdefault_に置き換えます。
  • ステートメントを式に置き換えます。
public enum Level
{
    One,
    Two,
    Three
}
public static int LevelToScore(Level level) => level switch
{
    Level.One   => 1,
    Level.Two   => 5,
    Level.Three => 10,
    _ => throw new ArgumentOutOfRangeException(nameof(level), $"Not expected level value: {level}"),
};

usingステートメント

C#8では、コンパイラーが宣言した変数をコードブロックの最後で処理するよう指示するusing宣言機能が追加されました。using宣言は、従来のusing文コードよりも簡潔です。どちらの方法でも、コンパイラーはコードブロックの最後にDisposeを呼び出します。

static void WriteLinesToFile(IEnumerable<string> lines)
{
    using var file = new System.IO.StreamWriter("WriteLines.txt");
    do somthing... ;
    return;
    // file is disposed here
}

索引と範囲

C#8ではインデックスと範囲の機能が追加され、シーケンス内の個々の要素または範囲にアクセスするための簡潔な構文が提供された。この構文は2つの新しい型と2つの新しい演算子に依存します:

  • System. Inde xはシーケンスインデックスです。
  • System.Rangeはシーケンスの部分範囲を表します。
  • 末尾演算子^は、数値を加算して最後の数を指定します。
  • 範囲演算子...、範囲の開始と終了を指定します。

範囲演算子には範囲の開始が含まれますが、範囲の終了は含まれません。

var words = new string[]
{               // 正常索引             索引对应的末尾运算符
    "The",      // 0                   ^9
    "quick",    // 1                   ^8
    "brown",    // 2                   ^7
    "fox",      // 3                   ^6
    "jumped",   // 4                   ^5
    "over",     // 5                   ^4
    "the",      // 6                   ^3
    "lazy",     // 7                   ^2
    "dog"       // 8                   ^1
};              // 9 (words.Length)    ^0
Console.WriteLine($"The last word is {words[^1]}"); // dog
var allWords = words[..]; // 包含所有值,等同于words[0..^0].
var firstPhrase = words[..4]; // 开始到words[4],不包含words[4]
var lastPhrase = words[6..]; // words[6]到末尾
// 声明范围变量
Range phrase = 1..4;
var text = words[phrase];

??与??=

??マージ演算子C#6以降で使用可能。左オペランドの値がnullでない場合は値を返します。それ以外の場合は、右のオペランドを評価し、結果を返します。左オペランドの評価結果がnull以外の場合、右オペランドは評価されません。

??=マージ代入演算子C#8以降で使用可能で、左オペランドの値がnullに評価された場合にのみ左オペランドに代入されます。それ以外の場合、右のオペランドは計算されません。=演算子の左オペランドは、変数、属性、またはインデクサ要素でなければなりません。

// ??合并运算符
Console.WriteLine($"name is {OutputName(null)}");
static string OutputName(string name) => name ?? "some one";
// 使用??=赋值运算符
variable ??= expression;
// 老写法
if (variable is null)
{
    variable = expression;
}

最上位のステートメント

C#9ではトップレベルステートメントが導入され、アプリケーションから不要なプロセスを削除し、トップレベルステートメントを使用できるファイルは1つだけです。最上位レベルのステートメントは、メインプログラムを読みやすくし、不要なスキーマ名前空間、class Program、static void Mainを削減します。

VSを使用してコマンドラインプロジェクトを作成し、. NET 5以上を選択すると、トップレベルのステートメントが使用されます。

// 使用VS2022创建.NET 6.0平台的命令行程序默认生成的内容
// See https://aka.ms/new-console-template for more information
Console.WriteLine("Hello, World!");

global using

C#10ではglobal usingディレクティブが追加されました。globalキーワードglobalがusingディレクティブの前に来ると、プロジェクト全体に適用されます。これにより、ファイルあたりのusingディレクティブの行数を減らすことができます。global usingディレクティブは任意のソースコードファイルの先頭に置くことができますが、グローバルでないusingの前に追加する必要があります。

global修飾子はstatic修飾子と一緒に使用することも、usingエイリアスディレクティブに適用することもできます。どちらの場合も、ディレクティブのスコープは現在コンパイル中のすべてのファイルです。

global using System;
global using static System.Console; // 全局静态导入
global using Env = System.Environment; // 全局别名

ファイル范囲の名前

C#10ではファイルスコープの名前空間が導入され、中括弧を追加せずにセミコロンが続く文として名前空間を含めることができる。コードファイルは通常1つの名前空間のみを含むため、コードが簡素化され、ネストの層がなくなります。ファイルスコープ名前空間は、ネストされた名前空間や2番目のファイルスコープ名前空間を宣言することはできず、型を宣言する前に、そのファイル内のすべての型がその名前空間に属しなければなりません。

using System;
namespace SampleFileScopedNamespace;
class SampleClass { }
interface ISampleInterface { }
struct SampleStruct { }
enum SampleEnum { a, b }
delegate void SampleDelegate(int i);

式を使用して

C#9ではwith式が導入され、変更された特定のプロパティとフィールドを使用して操作オブジェクトのコピーを生成し、変更されていない値は元のオブジェクトと同じ値を保持する。参照型メンバーの場合、オペランドがコピーされると、そのメンバーのインスタンスへの参照のみがコピーされます。with式によって生成されたコピーと元のオブジェクトの両方が、同じ参照型インスタンスへのアクセス権を持ちます。

C#9では、with式の左オペランドはrecord型でなければならなかったが、C#10ではwith式の左オペランドも構造体型であることができるように改良された。

public record NamedPoint(string Name, int X, int Y);
var p1 = new NamedPoint("A", 0, 0);
var p2 = p1 with { Name = "B", X = 5 };
Keep Exploring

延伸阅读

更多文章
同分类 / 同标签 2026/04/22

バージョン別の. NETサポート状況(250 7 0 7更新)

仮想マシンとテストマシンを使用して、各バージョンのオペレーティングシステムの. NETサポートをテストします。オペレーティングシステムのインストール後、対応するランタイムを測定し、スターダストエージェントをパスとして実行できます。

继续阅读
同分类 / 同标签 2026/02/07

AOTの使用経験

プロジェクトの最初から、新しい機能が追加されたり、新しい構文が使用されたりするたびに、AOTリリーステストを行うという良い習慣を身につける必要があります。

继续阅读