C# 7.0 の機能

C# 7.0 は C# の 7 番目のバージョンです。このバージョンにはいくつかの新機能が含まれています:タプルの言語サポート、ローカル関数、out var 宣言、桁区切り、バイナリ リテラル、パターン マッチング、スロー式、ref returnref local および拡張式本文メンバー リスト。

公式リファレンス:What's new in C# 7

# タプルの言語サポート

# 基本

タプル 要素の順序付けられた有限リストです。タプルは、タプルの各要素を個別に操作するのではなく、1 つのエンティティをまとめて操作し、リレーショナル データベースの個々の行 (つまり、「レコード」) を表す手段として、プログラミングで一般的に使用されます。

C# 7.0 では、メソッドは複数の戻り値を持つことができます。舞台裏では、コンパイラは新しい ValueTuple 構造体を使用します。

public (int sum, int count) GetTallies() 
{
    return (1, 2);
}

補足 :これを Visual Studio 2017 で機能させるには、System.ValueTuple を取得する必要があります パッケージ。

タプルを返すメソッドの結果が単一の変数に割り当てられている場合、メソッド シグネチャで定義された名前によってメンバーにアクセスできます。

var result = GetTallies();
// > result.sum
// 1
// > result.count
// 2

# タプルの分解

タプルの分解は、タプルをその部分に分離します。

たとえば、GetTallies を呼び出します。 戻り値を 2 つの個別の変数に割り当てると、タプルがこれら 2 つの変数に分解されます。

(int tallyOne, int tallyTwo) = GetTallies();

var も機能します:

(var s, var c) = GetTallies();

var を使用して、より短い構文を使用することもできます () の外 :

var (s, c) = GetTallies();

既存の変数に分解することもできます:

int s, c;
(s, c) = GetTallies();

スワップがはるかに簡単になりました (temp 変数は必要ありません):

(b, a) = (a, b);

興味深いことに、 Deconstruct を定義することで、あらゆるオブジェクトを分解できます。 クラスのメソッド:

class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public void Deconstruct(out string firstName, out string lastName)
    {
        firstName = FirstName;
        lastName = LastName;
    }
}

var person = new Person { FirstName = "John", LastName = "Smith" };
var (localFirstName, localLastName) = person;

この場合、(localFirstName, localLastName) = person 構文は Deconstruct を呼び出しています person で .

分解は、拡張メソッドで定義することもできます。これは上記と同等です:

public static class PersonExtensions
{
    public static void Deconstruct(this Person person, out string firstName, out string lastName)
    {
        firstName = person.FirstName;
        lastName = person.LastName;
    }
}

var (localFirstName, localLastName) = person;

Person の代替アプローチ クラスは Name を定義することです それ自体を Tuple として .以下を検討してください:

class Person
{
    public (string First, string Last) Name { get; }

    public Person((string FirstName, string LastName) name)
    {
        Name = name;
    }
}

次に、次のように person をインスタンス化できます (引数としてタプルを取ることができます):

var person = new Person(("Jane", "Smith"));

var firstName = person.Name.First; // "Jane"
var lastName = person.Name.Last;   // "Smith"

# タプルの初期化

コードで任意にタプルを作成することもできます:

var name = ("John", "Smith");
Console.WriteLine(name.Item1);
// Outputs John

Console.WriteLine(name.Item2);
// Outputs Smith

#

タプルを作成するとき、タプルのメンバーにアドホック アイテム名を割り当てることができます:

var name = (first: "John", middle: "Q", last: "Smith");
Console.WriteLine(name.first);
// Outputs John

# 型推論

同じシグネチャ (一致する型と数) で定義された複数のタプルは、一致する型として推論されます。例:

public (int sum, double average) Measure(List<int> items)
{
    var stats = (sum: 0, average: 0d);
    stats.sum = items.Sum();
    stats.average = items.Average();
    return stats;
}

stats stats の宣言以降に返すことができます 変数とメソッドのリターン シグネチャが一致します。

# リフレクションおよびタプル フィールド名

メンバー名は実行時には存在しません。リフレクションは、メンバー名が一致しなくても、メンバーの数とタイプが同じタプルを同じと見なします。タプルを object に変換する 次に、メンバーの型が同じで名前が異なるタプルに対しても、例外は発生しません。

ValueTuple クラス自体はメンバー名の情報を保持しませんが、情報は TupleElementNamesAttribute のリフレクションを通じて利用できます。この属性は、タプル自体には適用されませんが、メソッド パラメーター、戻り値、プロパティ、およびフィールドに適用されます。これにより、アセンブリ間でタプル項目名を保持できます。つまり、メソッドが (string name, int count) を返す場合、名前 name と count は、値を含む TupleElementNameAttribute で戻り値がマークされるため、別のアセンブリのメソッドの呼び出し元で使用できます。 「名前」と「カウント」。

# ジェネリックと async で使用

新しいタプル機能 (基礎となる ValueTuple を使用) type) はジェネリックを完全にサポートし、ジェネリック型パラメーターとして使用できます。これにより、async で使用できるようになります。 /await パターン:

public async Task<(string value, int count)> GetValueAsync()
{
    string fooBar = await _stackoverflow.GetStringAsync();
    int num = await _stackoverflow.GetIntAsync();

    return (fooBar, num);
}

# コレクションで使用

コードの分岐を避けるために、条件に基づいて一致するタプルを見つけようとするシナリオ (例として) では、タプルのコレクションを持つことが有益になる場合があります。

例:

private readonly List<Tuple<string, string, string>> labels = new List<Tuple<string, string, string>>()
{
    new Tuple<string, string, string>("test1", "test2", "Value"),
    new Tuple<string, string, string>("test1", "test1", "Value2"),
    new Tuple<string, string, string>("test2", "test2", "Value3"),
};

public string FindMatchingValue(string firstElement, string secondElement)
{
    var result = labels
        .Where(w => w.Item1 == firstElement && w.Item2 == secondElement)
        .FirstOrDefault();

    if (result == null)
        throw new ArgumentException("combo not found");

    return result.Item3;
}

新しいタプルは次のようになります:

private readonly List<(string firstThingy, string secondThingyLabel, string foundValue)> labels = new List<(string firstThingy, string secondThingyLabel, string foundValue)>()
{
    ("test1", "test2", "Value"),
    ("test1", "test1", "Value2"),
    ("test2", "test2", "Value3"),
}

public string FindMatchingValue(string firstElement, string secondElement)
{
    var result = labels
        .Where(w => w.firstThingy == firstElement && w.secondThingyLabel == secondElement)
        .FirstOrDefault();

    if (result == null)
        throw new ArgumentException("combo not found");

    return result.foundValue;
}

上記の例のタプルの命名は非常に一般的ですが、関連するラベルのアイデアにより、「item1」、「item2」、および「item3」を参照するよりも、コードで何が試みられているかをより深く理解できます。

# ValueTuple と Tuple の違い

ValueTuple導入の主な理由 はパフォーマンスです。

型名 ValueTuple Tuple
クラスまたは構造体 struct class
可変性 (作成後の値の変更) 可変 不変
メンバーの命名と他の言語のサポート はい いいえ (TBD )

# 件の参照

  • GitHub での元のタプル言語機能の提案
  • C# 7.0 機能用の実行可能な VS 15 ソリューション
  • NuGet タプル パッケージ

# ローカル関数

ローカル関数はメソッド内で定義され、メソッド外では使用できません。すべてのローカル変数にアクセスでき、イテレータ async をサポートします。 /await そしてラムダ構文。このようにして、クラスを混雑させることなく、関数に固有の繰り返しを関数化できます。副作用として、これによりインテリセンスの提案のパフォーマンスが向上します。

# 例

double GetCylinderVolume(double radius, double height)
{
    return getVolume();

    double getVolume()
    {
        // You can declare inner-local functions in a local function 
        double GetCircleArea(double r) => Math.PI * r * r;

        // ALL parents' variables are accessible even though parent doesn't have any input. 
        return GetCircleArea(radius) * height;
    }
}

ローカル関数は、LINQ 演算子のコードを大幅に簡素化します。通常、引数チェックを実際のロジックから分離して、反復が開始されるまで遅延することなく、引数チェックを即座に行う必要があります。

# 例

public static IEnumerable<TSource> Where<TSource>(
    this IEnumerable<TSource> source, 
    Func<TSource, bool> predicate)
{
    if (source == null) throw new ArgumentNullException(nameof(source));
    if (predicate == null) throw new ArgumentNullException(nameof(predicate));

    return iterator();

    IEnumerable<TSource> iterator()
    {
        foreach (TSource element in source)
            if (predicate(element))
                yield return element;
    }
}

ローカル関数も async をサポートします および await

# 例

async Task WriteEmailsAsync()
{
    var emailRegex = new Regex(@"(?i)[a-z0-9_.+-]+@[a-z0-9-]+\.[a-z0-9-.]+");
    IEnumerable<string> emails1 = await getEmailsFromFileAsync("input1.txt");
    IEnumerable<string> emails2 = await getEmailsFromFileAsync("input2.txt");
    await writeLinesToFileAsync(emails1.Concat(emails2), "output.txt");

    async Task<IEnumerable<string>> getEmailsFromFileAsync(string fileName)
    {
        string text;

        using (StreamReader reader = File.OpenText(fileName))
        {
            text = await reader.ReadToEndAsync();
        }

        return from Match emailMatch in emailRegex.Matches(text) select emailMatch.Value;
    }

    async Task writeLinesToFileAsync(IEnumerable<string> lines, string fileName)
    {
        using (StreamWriter writer = File.CreateText(fileName))
        {
            foreach (string line in lines)
            {
                await writer.WriteLineAsync(line);
            }
        }
    }
}

お気づきかもしれませんが、ローカル関数は return の下で定義できることが重要です。 ステートメント、彼らはしません その上で定義する必要があります。さらに、ローカル関数は通常、「lowerCamelCase」命名規則に従い、クラス スコープ関数との区別が容易になります。

# out var 宣言

C# の一般的なパターンは bool TryParse(object input, out object value) を使用することです オブジェクトを安全に解析します。

out var 宣言は、読みやすさを改善するための単純な機能です。 out パラメーターとして渡されると同時に変数を宣言できます。

この方法で宣言された変数は、宣言された時点で本体の残りの部分にスコープされます。

# 例

TryParse の使用 C# 7.0 より前のバージョンでは、関数を呼び出す前に値を受け取る変数を宣言する必要があります:

int value;
if (int.TryParse(input, out value)) 
{
    Foo(value); // ok
}
else
{
    Foo(value); // value is zero
}

Foo(value); // ok

C# 7.0 では、out に渡される変数の宣言をインライン化できます。 パラメーターを使用することで、別の変数宣言が不要になります:

if (int.TryParse(input, out var value)) 
{
    Foo(value); // ok
}
else
{
    Foo(value); // value is zero
}

Foo(value); // still ok, the value in scope within the remainder of the body

out で関数が返すパラメータの一部が discard を使用できます 演算子 _ .

p.GetCoordinates(out var x, out _); // I only care about x

out var 宣言は、すでに out を持つ既存の関数で使用できます パラメーター。関数宣言の構文は同じままで、関数を out var と互換性を持たせるために追加の要件は必要ありません。 宣言。この機能は単なる構文糖衣です。

out var のもう 1 つの機能 宣言は、匿名型で使用できることです。

var a = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
var groupedByMod2 = a.Select(x => new
                                  {
                                      Source = x,
                                      Mod2 = x % 2
                                  })
                     .GroupBy(x => x.Mod2)
                     .ToDictionary(g => g.Key, g => g.ToArray());
if (groupedByMod2.TryGetValue(1, out var oddElements))
{
    Console.WriteLine(oddElements.Length);
}

このコードでは、Dictionary を作成します。 int で 匿名型の値のキーと配列。以前のバージョンの C# では TryGetValue を使用できませんでした out を宣言する必要があるため、ここでメソッドを使用します。 変数 (これは匿名型です!)。ただし、out var では out の型を明示的に指定する必要はありません

# 制限

out var 宣言は、式が式のラムダ本体として解釈されるため、LINQ クエリでの使用が制限されていることに注意してください。そのため、導入された変数のスコープはこれらのラムダに限定されます。たとえば、次のコードは機能しません:

var nums = 
    from item in seq
    let success = int.TryParse(item, out var tmp)
    select success ? tmp : 0; // Error: The name 'tmp' does not exist in the current context

# 件の参照

  • GitHub での元の out var 宣言の提案

# パターン マッチング

C# のパターン マッチング拡張機能は、関数型言語からのパターン マッチングの利点の多くを有効にしますが、基礎となる言語の感覚とスムーズに統合する方法で

# switch 表現

パターン マッチングは switch を拡張します タイプをオンにするステートメント:

class Geometry {} 

class Triangle : Geometry
{
    public int Width { get; set; }
    public int Height { get; set; }
    public int Base { get; set; }
}

class Rectangle : Geometry
{
    public int Width { get; set; }
    public int Height { get; set; }
}

class Square : Geometry
{
    public int Width { get; set; }
}

public static void PatternMatching()
{
    Geometry g = new Square { Width = 5 }; 
    
    switch (g)
    {
        case Triangle t:
            Console.WriteLine($"{t.Width} {t.Height} {t.Base}");
            break;
        case Rectangle sq when sq.Width == sq.Height:
            Console.WriteLine($"Square rectangle: {sq.Width} {sq.Height}");
            break;
        case Rectangle r:
            Console.WriteLine($"{r.Width} {r.Height}");
            break;
        case Square s:
            Console.WriteLine($"{s.Width}");
            break;
        default:
            Console.WriteLine("<other>");
            break;
    }
}

# is 表現

パターン マッチングは is を拡張します 演算子を使用して型をチェックし、同時に新しい変数を宣言します。

# 例

string s = o as string;
if(s != null)
{
    // do something with s
}

次のように書き換えることができます:

if(o is string s)
{
    //Do something with s
};

パターン変数 s のスコープにも注意してください。 if の外まで拡張されます 囲んでいるスコープの終わりに到達するブロック、例:

if(someCondition)
{
   if(o is string s)
   {
      //Do something with s
   }
   else
   {
     // s is unassigned here, but accessible 
   }

   // s is unassigned here, but accessible 
}
// s is not accessible here

# 桁区切り

アンダースコア _ 桁区切りとして使用できます。大きな数値リテラルで数字をグループ化できることは、読みやすさに大きな影響を与えます。

アンダースコアは、以下に示す場合を除き、数値リテラルのどこにでも使用できます。異なるシナリオや異なる数値ベースでは、異なるグループ化が意味を持つ場合があります。

任意の一連の数字は、1 つまたは複数のアンダースコアで区切ることができます。 _ 指数だけでなく小数でも使用できます。区切り記号には意味的な影響はありません。単に無視されます。

int bin = 0b1001_1010_0001_0100;
int hex = 0x1b_a0_44_fe;
int dec = 33_554_432;
int weird = 1_2__3___4____5_____6______7_______8________9;
double real = 1_000.111_1e-1_000;

_ の場所 桁区切り記号は使用できません:

  • 値の先頭 (_121 )
  • 値の末尾 (121_ または 121.05_ )
  • 小数点以下 (10_.0 )
  • 指数文字の隣 (1.1e_1 )
  • 型指定子の横 (10_f )
  • 0x の直後 または 0b 2 進数および 16 進数のリテラル (例:0b_1001_1000 を許可するように変更される場合があります)

# バイナリ リテラル

0b プレフィックスを使用してバイナリ リテラルを表すことができます。

2 進数リテラルを使用すると、0 と 1 から数値を構成できるため、数値の 2 進数表現でどのビットが設定されているかを簡単に確認できます。これは、バイナリ フラグを操作するのに役立ちます。

以下は int を指定する同等の方法です 値 34 で (=2 5 + 2 1 ):

// Using a binary literal:
//   bits: 76543210
int a1 = 0b00100010;          // binary: explicitly specify bits

// Existing methods:
int a2 = 0x22;                // hexadecimal: every digit corresponds to 4 bits
int a3 = 34;                  // decimal: hard to visualise which bits are set
int a4 = (1 << 5) | (1 << 1); // bitwise arithmetic: combining non-zero bits

# フラグ列挙

enum のフラグ値を指定する前に この例の 3 つの方法のいずれかを使用してのみ行うことができます:

[Flags]
public enum DaysOfWeek
{
    // Previously available methods:
    //          decimal        hex       bit shifting
    Monday    =  1,    //    = 0x01    = 1 << 0
    Tuesday   =  2,    //    = 0x02    = 1 << 1
    Wednesday =  4,    //    = 0x04    = 1 << 2
    Thursday  =  8,    //    = 0x08    = 1 << 3
    Friday    = 16,    //    = 0x10    = 1 << 4
    Saturday  = 32,    //    = 0x20    = 1 << 5
    Sunday    = 64,    //    = 0x40    = 1 << 6

    Weekdays = Monday | Tuesday | Wednesday | Thursday | Friday,
    Weekends = Saturday | Sunday
}

バイナリ リテラルを使用すると、どのビットが設定されているかがより明確になり、それらを使用するために 16 進数やビットごとの算術演算を理解する必要はありません。

[Flags]
public enum DaysOfWeek
{
    Monday    = 0b00000001,
    Tuesday   = 0b00000010,
    Wednesday = 0b00000100,
    Thursday  = 0b00001000,
    Friday    = 0b00010000,
    Saturday  = 0b00100000,
    Sunday    = 0b01000000,

    Weekdays = Monday | Tuesday | Wednesday | Thursday | Friday,
    Weekends = Saturday | Sunday
}

# スロー式

C# 7.0 では、特定の場所で式としてスローすることができます:

class Person
{
    public string Name { get; }

    public Person(string name) => Name = name ?? throw new ArgumentNullException(nameof(name));

    public string GetFirstName()
    {
        var parts = Name.Split(' ');
        return (parts.Length > 0) ? parts[0] : throw new InvalidOperationException("No name!");
    }

    public string GetLastName() => throw new NotImplementedException();
}

C# 7.0 より前では、式の本体から例外をスローする場合は、次のことを行う必要がありました:

var spoons = "dinner,desert,soup".Split(',');

var spoonsArray = spoons.Length > 0 ? spoons : null;

if (spoonsArray == null) 
{
    throw new Exception("There are no spoons");
}

または

var spoonsArray = spoons.Length > 0 
    ? spoons 
    : new Func<string[]>(() => 
      {
          throw new Exception("There are no spoons");
      })();

C# 7.0 では、上記は次のように単純化されています。

var spoonsArray = spoons.Length > 0 ? spoons : throw new Exception("There are no spoons");

# 拡張式の本文メンバー リスト

C# 7.0 では、アクセサー、コンストラクター、およびファイナライザーが、式の本体を持つことができるもののリストに追加されています。

class Person
{
    private static ConcurrentDictionary<int, string> names = new ConcurrentDictionary<int, string>();

    private int id = GetId();

    public Person(string name) => names.TryAdd(id, name); // constructors

    ~Person() => names.TryRemove(id, out _);              // finalizers

    public string Name
    {
        get => names[id];                                 // getters
        set => names[id] = value;                         // setters
    }
}

破棄演算子については、out var 宣言セクションも参照してください。

# ref リターンと ref ローカル

ref リターンと ref ローカルは、安全でないポインターに頼らずにメモリをコピーする代わりに、メモリ ブロックへの参照を操作して返すのに役立ちます。

# 参照リターン

public static ref TValue Choose<TValue>(
    Func<bool> condition, ref TValue left, ref TValue right)
{
    return condition() ? ref left : ref right;
}

これにより、参照によって 2 つの値を渡すことができ、そのうちの 1 つが何らかの条件に基づいて返されます:

Matrix3D left = …, right = …;
Choose(chooser, ref left, ref right).M20 = 1.0;

# 参照ローカル

public static ref int Max(ref int first, ref int second, ref int third)
{
    ref int max = first > second ? ref first : ref second;
    return max > third ? ref max : ref third;
}
…
int a = 1, b = 2, c = 3;
Max(ref a, ref b, ref c) = 4;
Debug.Assert(a == 1); // true
Debug.Assert(b == 2); // true
Debug.Assert(c == 4); // true

# 安全でない参照操作

System.Runtime.CompilerServices.Unsaferef を操作できる一連の安全でない操作が定義されています 基本的に、ポインタであるかのように値を返します。

たとえば、メモリ アドレス (ref) を再解釈すると、 ) 別のタイプとして:

byte[] b = new byte[4] { 0x42, 0x42, 0x42, 0x42 };

ref int r = ref Unsafe.As<byte, int>(ref b[0]);
Assert.Equal(0x42424242, r);

0x0EF00EF0;
Assert.Equal(0xFE, b[0] | b[1] | b[2] | b[3]);

ただし、これを行うときはエンディアンに注意してください。 BitConverter.IsLittleEndian をチェック 必要に応じて適切に処理してください。

または、安全でない方法で配列を反復処理します:

int[] a = new int[] { 0x123, 0x234, 0x345, 0x456 };

ref int r1 = ref Unsafe.Add(ref a[0], 1);
Assert.Equal(0x234, r1);

ref int r2 = ref Unsafe.Add(ref r1, 2);
Assert.Equal(0x456, r2);

ref int r3 = ref Unsafe.Add(ref r2, -3);
Assert.Equal(0x123, r3);

または同様の Subtract :

string[] a = new string[] { "abc", "def", "ghi", "jkl" };

ref string r1 = ref Unsafe.Subtract(ref a[0], -2);
Assert.Equal("ghi", r1);

ref string r2 = ref Unsafe.Subtract(ref r1, -1);
Assert.Equal("jkl", r2);

ref string r3 = ref Unsafe.Subtract(ref r2, 3);
Assert.Equal("abc", r3);

さらに、2 つの ref かどうかを確認できます。 値が同じ、つまり同じアドレス:

long[] a = new long[2];

Assert.True(Unsafe.AreSame(ref a[0], ref a[0]));
Assert.False(Unsafe.AreSame(ref a[0], ref a[1]));

Roslyn Github の問題

github の System.Runtime.CompilerServices.Unsafe

# ValueTask

Task<T> クラスです 結果がすぐに利用可能になると、割り当ての不要なオーバーヘッドが発生します。

ValueTask<T> 構造です Task の割り当てを防ぐために導入されました async の結果の場合のオブジェクト 待っている時点で操作はすでに利用可能です。

だから ValueTask<T> 2 つのメリットがあります:

# 1. パフォーマンスの向上

これが Task<T> です 例:

  • ヒープ割り当てが必要
  • JIT で 120ns かかる
async Task<int> TestTask(int d)
{
    await Task.Delay(d);
    return 10;
}

これがアナログの ValueTask<T> です 例:

  • 結果が同期的に知られている場合、ヒープ割り当てはありません (この場合は Task.Delay のため、そうではありません) 、しかし多くの場合、多くの現実世界で async /await シナリオ)
  • JIT で 65ns かかる
async ValueTask<int> TestValueTask(int d)
{
    await Task.Delay(d);
    return 10;
}

# 2. 実装の柔軟性の向上

同期を希望する非同期インターフェースの実装は、それ以外の場合は Task.Run のいずれかを使用する必要があります。 または Task.FromResult (その結果、前述のパフォーマンス ペナルティが発生します)。したがって、同期の実装に対するプレッシャーがあります。

しかし ValueTask<T> では 、呼び出し元に影響を与えることなく、実装は同期か非同期かをより自由に選択できます。

たとえば、非同期メソッドを使用したインターフェイスは次のとおりです:

interface IFoo<T>
{
    ValueTask<T> BarAsync();
}

...そして、そのメソッドを呼び出す方法は次のとおりです:

IFoo<T> thing = getThing();
var x = await thing.BarAsync();

ValueTask で 、上記のコードは同期または非同期の実装で動作します :

# 同期実装:

class SynchronousFoo<T> : IFoo<T>
{
    public ValueTask<T> BarAsync()
    {
        var value = default(T);
        return new ValueTask<T>(value);
    }
}

# 非同期実装

class AsynchronousFoo<T> : IFoo<T>
{
    public async ValueTask<T> BarAsync()
    {
        var value = default(T);
        await Task.Delay(1);
        return value;
    }
}

# メモ

ValueTask でも struct は C# 7.0 に追加される予定でしたが、当面は別のライブラリとして保持されています。ValueTask System.Threading.Tasks.Extensions パッケージは Nuget ギャラリーからダウンロードできます