June 2018

Volume 33 Number 6

.Net framework の組の問題:ガイドラインを中断する c# の組を取得する理由

によってマーク Michaelis |年 6 月 2018

バックアップの MSDN マガジンの 2017 年 8 月の問題は詳細な記事を書きました c# 7.0 および組のサポート (msdn.com/magazine/mt493248)。時にこと、タプル型で導入された c# 7.0 (内部型 ValueTuple <>...]) の区切りに適切に構成された値型のガイドラインがいくつかつまりファクトはパーミッション。

• はパブリックまたはプロテクト フィールドを宣言していません (代わりに、プロパティを持つカプセル化する)。

• 変更可能な値の型を定義します。

• 作成しないで値型 16 バイトより大きいサイズ。

次のガイドライン実施されている c# 1.0 では、以降、まだここで 7.0 では C# の場合、それらしたされてスロー System.ValueTuple <>... データ型を定義する風に。技術的には、System.ValueTuple <>... は異なるアリティ (具体的には、型パラメーターの数) ですが、同じ名前のデータ型のファミリです。この時間の長い尊重のガイドラインが適用されなくなった特定のデータ型に関する特別な何ですか。次のガイドラインが適用される状況を理解する方法は、— または適用しない-値の型を定義する場合に、アプリケーションの改良にご協力しますか?

カプセル化およびフィールドとプロパティの利点に重点を置いて、ディスカッションを開始してみましょう。たとえば、円の円周の部分を表す円弧を値型を検討します。ように、円、開始角度 (度単位) に向けて、円弧の最初のポイントの掃引角度 (度単位)、円弧の終点の半径によって定義された図 1です。

図 1 の円弧の定義

public struct Arc
{
  public Arc (double radius, double startAngle, double sweepAngle)
  {
    Radius = radius;
    StartAngle = startAngle;
    SweepAngle = sweepAngle;
  }

  public double Radius;
  public double StartAngle;
  public double SweepAngle;

  public double Length
  {
    get
    {
      return Math.Abs(StartAngle - SweepAngle)
        / 360 * 2 * Math.PI * Radius;
    }
  }

  public void Rotate(double degrees)
  {
    StartAngle += degrees;
    SweepAngle += degrees;
  }

  // Override object.Equals
  public override bool Equals(object obj)
  {
    return (obj is Arc)
      && Equals((Arc)obj);
  }

        // Implemented IEquitable<T>
  public bool Equals(Arc arc)
  {
    return (Radius, StartAngle, SweepAngle).Equals(
      (arc.Radius, arc.StartAngle, arc.SweepAngle));
  }

  // Override object.GetHashCode
  public override int GetHashCode() =>
    return (Radius, StartAngle, SweepAngle).GetHashCode();

  public static bool operator ==(Arc lhs, Arc rhs) =>
    lhs.Equals(rhs);

  public static bool operator !=(Arc lhs, Arc rhs) =>
    !lhs.Equals(rhs);
}

パブリックまたはプロテクト フィールドを宣言しません

この宣言で円弧、円弧の特性を定義する 3 つのパブリック フィールドを持つ (キーワード構造体を使用して定義されている) 値型です。[はい]、プロパティを使用した可能性がありますが、最初のガイドラインに違反しているので、具体的には、この例ではパブリック フィールドを使用すると選択した-パブリックまたはプロテクト フィールドを宣言しません。

プロパティではなく、パブリック フィールドを活用するには、円弧の定義がないオブジェクト指向設計原則の最も基本的な — カプセル化します。たとえば、場合しました、radius を使用して、掃引角度ではなく角度と円弧の長さをたとえば、開始を内部データ構造を変更しますか。そう言うまでもなくを使用できなくなります、インターフェイスの円弧とのすべてのクライアントは、コードの変更を適用するとします。

同様に、Radius、StartAngle および SweepAngle の定義、あるありません検証。Radius、たとえば、割り当てでした、負の値。および StartAngle と SweepAngle に負の値が許容される可能性がありますが、360 ° より大きい値はありません。残念ながら、パブリック フィールドを使用して、円弧が定義されていること、ためこれらの値を防ぐために検証を追加する方法はありません。はい、検証、プロパティ、フィールドを変更することによってバージョン 2 の追加でしたが、円弧構造のバージョンの互換性が中断するようです。コンパイル済みのフィールドを呼び出すコードを既存のすべてを使用できなくなります実行時に、としてはいずれかのコード (再コンパイル) 場合でもとしてフィールドを渡す、ref パラメーターでします。

フィールドがされないこと public または protected のガイドラインが既定値を持つ特にのプロパティがよりプロパティ初期化子の c# 6.0 でサポートするために感謝のプロパティでカプセル化された明示的なフィールドを定義するが容易になりましたことに注目すべきです。たとえば、次のコード

public double SweepAngle { get; set; } = 180;

これよりも簡単です。

private double _SweepAngle = 180;

public double SweepAngle {
  get { return _SweepAngle; }
  set { _SweepAngle = value; }
}

プロパティの初期化子のサポートがあるため、これがないと、自動的に実装されたプロパティの初期化を必要がある必要があります、付随するコンス トラクターです。結果として、ガイドライン。「フィールドに自動的に実装されたプロパティを検討してください」(さらにプライベート フィールド) では、両方不要になったことができます、コードがより簡潔なため、変更、包含するプロパティの外部からのフィールドを検出します。このすべては、「避けそのを含むプロパティの外部からのフィールドにアクセスする」他のクラス メンバーからも説明されている以前のデータのカプセル化の原則を強調する、さらに別のガイドラインを優先します。

この時点で、c# 7.0 タプル型 ValueTuple <>... に戻ることができます。、公開されているフィールドに関する規定にかかわらず ValueTuple < T1, T2 > たとえば、以下のとおり。

public struct ValueTuple<T1, T2>
  : IComparable<ValueTuple<T1, T2>>, ...
{
  public T1 Item1;
  public T2 Item2;
  // ...
}

ValueTuple <>... 特別の特長は、ほとんどのデータ構造体とは異なり c# 7.0 タプル、組と呼ばれるそのは全体のオブジェクト (ユーザーまたは CardDeck オブジェクトなど) に関するされませんでした。代わりに、ref パラメーターまたはを使用しての手間せず、メソッドから返される可能性がありますが、トランスポート用に、任意にグループ化する個々 の部分についてでした。Torgersen は一連の同じバスに含まれるユーザーとの類似性を mads — バス タプルのようにあり、ユーザーは、タプル内の項目に似ています。アイテムにまとめられています、戻り値の組パラメーターそれらすべて宛て、呼び出し元に戻るにはないが必ずしも他の関連付けを相互にあるためです。実際に、呼び出し元が、組から値を取得し、単位としてではなく、個別に扱うことと可能性が高いです。

全体ではなく、個々 のアイテムの重要度は、小さい説得力のあるカプセル化の概念です。考えれば、組内の項目は、互いに完全に関連することができます、たとえば、Item1 を変更すると Item2 が変わる可能性があります、このような方法でそれらをカプセル化する必要は多くの場合はありません。(これに対し、円弧の長さの変更が必要、角度の一方または両方の変更のカプセル化が必要であるためです。) さらに、組の中で格納されている項目に無効な値はありません。いずれかの検証は、項目自体のデータ型ではなく組の項目のプロパティのいずれかの割り当てに強制されます。

このため、組のプロパティは、任意の値を指定できないし、提供することが考えられる将来の値はありません。つまり、データが含まれる検証の必要なしに変更可能な型を定義する場合は、フィールドをも使用できます。プロパティを活用することができますが、getter と setter の間でさまざまなアクセシビリティを持つされます。ただし、可変性が許容されると仮定した場合、ことはありません活用するためにプロパティ get アクセス操作子と set アクセス操作子のアクセシビリティが異なるでいずれか。このすべては、別の問題を発生させます — する必要があります、タプル型変更可能ですか?。

変更可能な値の型が定義されていません

次のガイドラインを考慮する必要は、変更可能な値の型です。円弧の例では、もう一度 (内のコードに示すように図 2) ガイドラインに違反しています。これを考慮する場合は明らかです-値の型と、コピー、コピーを変更することも、呼び出し元から観測可能なオブジェクトはできません。ただし、内のコードを while図 2のみ、コピー、コードの読みやすさを変更した場合の概念がないを示しています。読みやすさの観点からよう円弧の変更に見えます。

図 2 の値の型がコピーされるは、呼び出し元は、変更を確認しないため

[TestMethod]
public void PassByValue_Modify_ChangeIsLost()
{
  void Modify(Arc paramameter) { paramameter.Radius++; }
  Arc arc = new Arc(42, 0, 90);
  Modify(arc);
  Assert.AreEqual<double>(42, arc.Radius);
}

複雑になる場合は、どのような値コピーの動作を期待する開発者のためには、円弧が値型をしたことを知っている必要があります。ただしは、ソース コードの動作を示す値の型 (ただし、公平にするには、データ型を合わせる場合は、Visual Studio IDE は構造体として値の型を表示) から明確に何も行われません。C# プログラマで値の型と参照型のセマンティクスを知っておく必要があるを考えるとでしたおそらくになるようで動作図 2が必要です。ただしでのシナリオを検討図 3コピー動作はすぐに見つからないものとします。

図 3 の変更可能な値の型が予期しない動作します。

public class PieShape
{
  public Point Center { get; }
  public Arc Arc { get; }

  public PieShape(Arc arc, Point center = default)
  {
    Arc = arc;
    Center = center;
  }
}

public class PieShapeTests
{
  [TestMethod]
  public void Rotate_GivenArcOnPie_Fails()
  {
    PieShape pie = new PieShape(new Arc(42, 0, 90));
    Assert.AreEqual<double>(90, pie.Arc.SweepAngle);
    pie.Arc.Rotate(42);
    Assert.AreEqual<double>(90, pie.Arc.SweepAngle);
  }
}

呼び出し円弧の回転機能、実際には、円弧を行っても決して回転するに注意してください。その理由は、この複雑になる場合の動作では、2 つの要因の組み合わせのためです。まず、円弧は、参照ではなく値で渡される原因となった値の型です。その結果、円グラフを起動しています。弧は、コンス トラクターでインスタンス化されている円弧の同じインスタンスを返すのではなく円弧のコピーを返します。2 番目の要素の場合に、この問題、でしょう。回転の呼び出しは、円グラフ内に格納された円弧のインスタンスを変更するためのものが、実際には、円弧プロパティから返されたコピーを変更します。あるガイドライン、「変更可能な値の型を定義して」ください。

前に、c# 7.0 内の組このガイドラインを無視してように、定義上、ValueTuple <>... 変更可能なパブリック フィールドを公開します。この違反、ValueTuple <>... であるにもかかわらず生じない円弧として同じ欠点があります。その理由は、項目のフィールドを使用して、タプルを変更する唯一の方法があります。ただし、c# コンパイラでは、(かどうかを含む型は参照型、値型または配列でもまたは他の種類のコレクション) を含む型から返されたフィールド (またはプロパティ) の変更を許可しません。たとえば、次のコードはコンパイルされません。

pie.Arc.Radius = 0;

またこのコードには。

pie.Arc.Radius++;

これらのステートメントが失敗して、メッセージ"エラー CS1612:変更できません 'PieShape.Arc' の戻り値、変数ではありません。" 言い換えると、ガイドラインは、必ずしも正確ではないです。すべての変更可能な値型を回避するのではなく、キーは、関数 (読み取り/書き込みプロパティは、許容される) の変更を回避するのには。活用したり、もちろんを想定していることに示すように値セマンティクス図 2組み込み値型の動作が必要になるように十分な明白なは。

作成しない値の型の 16 バイトより大きい

このガイドラインは、値の型をコピーする頻度のために必要です。実際には、を除き、ref または out パラメーターには、値型は、事実上それらにアクセスするたびにします。これは、別に値型の 1 つのインスタンスを割り当てるかどうかに当てはまります (円弧などに弧を =図 3) またはメソッドの呼び出し (に示すように Modify(arc) など図 2)。パフォーマンス向上のためのガイドラインは、値型のサイズを小さくするのにです。

現実として ValueTuple <>... できますのサイズ多くの場合にする 128 ビット (16 バイト) よりも大きいため ValueTuple <>... 7 つの個々 のアイテム (およびそれ以上、8 番目の型パラメーターに対して別の組を指定する場合) を含めることができます。理由を次に、c# 7.0 タプル型として定義されて、値しますか。

必要な複雑な構文を使用しないで、複数の戻り値を有効にする言語機能としてタプルが導入された前述のように、out または ref パラメーター。一般的なパターンは、次に、構築、タプルを返すし、呼び出し元に戻って分解にでした。実際には、戻り値パラメーターを使用して下位のスタックの組を渡すことは、グループのスタックをメソッドの呼び出しの引数を渡すことに似ています。つまり、メモリ コピーがに関しては、戻り値の組は個々 のパラメーター リストを持つ対称です。

参照型としてタプルを宣言したかどうかは、ヒープ上の型を構築し、項目の値で初期化する必要があります-可能性のあるヒープへの参照または値をコピーします。どちらにしても、メモリ コピー操作は必須で、値型のメモリにコピーするときと同様です。さらに、参照組が不要になったアクセス可能な時間で後で、ガベージ コレクターは、メモリを回復する必要があります。つまり、参照の組は必要メモリをコピーするだけでなく、ガベージ コレクターより効率的なオプションの値型のタプルを行う追加の負荷。(まれなケースで値の組み合わせより効率的ではないことでしたまだに頼る参照型のバージョン、組 <>...。)

アーティクルの主なトピックと完全に一致しない、ときに、Equals や GetHashCode での実装に注意してください図 1です。組が Equals や GetHashCode を実装するためのショートカットを提供する方法を確認できます。詳細については、「上書き等値演算子および GetHashCode を使用する組です。」を参照してください。

まとめ

一見する組の変更できない値の型として定義することにより意外見えることができます。結局のところ、.NET Core と .NET Framework 内で検出された変更できない値型の数が最小限に抑えるは長年にわたるプログラミング ガイドライン変更できないプロパティを持つカプセル化されるようにするための値の型を呼び出すことがあります。F#、c# 言語デザイナーで提供する変数を変更できないか、変更不可能な型を定義、短縮形を求められている変更できない既定アプローチ特性の影響もあります。(このような短縮形ではない現在 (C#) 8.0 上の考慮事項 [読み取り専用の構造体に追加された C# 7.2 構造体が変更可能なことを確認するための手段として。)

ただし、詳しくと、重要な要素の数が表示されます。これらのタスクには次が含まれています。

• 参照型では、ガベージ コレクションを使用して、追加のパフォーマンスに与える影響を強制します。

• 組は一般に短期です。

• 組の項目には、カプセル化のプロパティを持つ予測可能な必要があるありません。

• でも組 (によって値型のガイドライン) 大きさではさらに組の参照実装の大量のメモリ コピー操作はありません。

要約すると、たくさんの標準的なガイドラインを行ってもパブリック フィールドを持つ値型のタプルを優先する要因です。最後は、ガイドラインは、その、ガイドラインだけを示します。それを無視しないですが、十分な指定-明示的に記載されていることをお勧め、および —、原因、OK では、行外になる場合に色をします。

値の型を定義して、Equals や GetHashCode をオーバーライドするためのガイドラインの詳細については、9 と 10 不可欠な c# 帳章をご覧ください。"重要な c# 7.0"(IntelliTect.com/EssentialCSharp)、内にあるが予期されています。


Mark Michaelis は、IntelliTect の創設者で、同社でチーフ テクニカル アーキテクト兼トレーナーを務めています。約 2 年間の彼は、Microsoft MVP され以来 Microsoft 地域ディレクター 2007年。いくつかの Microsoft ソフトウェアの設計について Michaelis 機能など、C# の場合、Mi crosoft Azure、SharePoint および Visual Studio ALM のチームを確認します。開発者カンファレンスで話す時など、最も最近使用した、多くの書籍書き込まれると彼は"Es sential c# 6.0 (第 5 版)」(itl.tc/EssentialCSharp)。連絡先は、Facebook (facebook.com/Mark.Michaelis、英語)、ブログ (IntelliTect.com/Mark、英語)、Twitter (@markmichaelis、英語)、または電子メール mark@IntelliTect.com (英語のみ) です。