2018 年 6 月

磁碟區 33 編號 6

本文章是由機器翻譯。

.Net framework Tuple 時發生問題:C# Tuple 為什麼收到中斷指導方針

標記 Michaelis |2018 年 6 月

回 2017 年 8 月發行的 MSDN Magazine 中撰寫相關的文章在 C# 7.0 和其支援之 tuple (msdn.com/magazine/mt493248)。在我已經說明的元組型別所導入的 C# 7.0 (在內部類型 ValueTuple <> …) 的符號數的指導方針,值型別的結構良好,也就是事實:

• 未宣告為公用或受保護的欄位 (而是封裝的內容)。

• 未定義可變動的實值類型。

• 不要建立實值型別大於 16 個位元組的大小。

這些指導方針就地自 C# 1.0,而且尚未這裡在 C# 7.0 中,它們已被擲回 wind System.ValueTuple <> … 資料型別定義。技術上來說,System.ValueTuple <>...是一系列資料型別相同的名稱,但不同的 arity (具體而言,型別參數的數目)。因此特殊有關這些長時間遵守指導方針不再套用這個特定的資料類型是什麼?內容,以及如何的情況下,這些指導方針適用於我們了解 — 或未套用 — 協助我們改善其應用程式,以定義實值型別嗎?

讓我們開始討論將焦點設在封裝及和欄位屬性的優點。例如,考慮弧線實值類型代表圓形周長部份。它由圓形中在弧線的第一個點的開始角度 (以度為單位),在弧線的最後一個點的掃掠角度 (以度為單位) 的半徑所定義中所示圖 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);
}

未宣告為公用或受保護的欄位

在這項宣告,弧線會是弧形的與定義特性的三個公用欄位 (定義使用關鍵字結構) 的值型別。是,我無法使用屬性,但是我選擇要使用在此範例中的公用欄位,特別是因為它違反了第一個的指導方針 — 請勿宣告為公用或受保護的欄位。

利用公用欄位,而不是屬性,弧線定義沒有最基本的物件導向設計原則,封裝 (encapsulation)。例如,如果我決定變更使用半徑、 開始角度和弧形的長度,比方說,而不是掃掠角度的內部資料結構嗎?這樣會很明顯地會破壞的弧形的介面和所有用戶端會強制變更程式。

同樣地,Radius 及 StartAngle SweepAngle 的定義,我會有任何驗證。Radius,例如,無法指派負值。而且雖然 StartAngle 和 SweepAngle 負數值可允許時,不會大於 360 度的值。不幸的是,弧線使用公用欄位來定義,因為沒有任何方法可以加入驗證,以防止這些值。是,我可以在第 2 版中新增驗證,藉由變更欄位屬性,但是這樣做會破壞的版本相容性弧線結構。任何現有已編譯的程式碼叫用的欄位會破壞在執行階段,以將任何程式碼 (即使重新編譯) 會傳遞做為欄位,由 ref 參數。

指定的欄位不應該為公用或受保護的指導方針,值得注意的是屬性,特別是使用預設值,變得更輕鬆地定義於封裝的屬性,以支援在 C# 6.0 中屬性初始設定式感謝您明確欄位。例如,下列程式碼:

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

會比這簡單:

private double _SweepAngle = 180;

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

屬性初始設定式的支援是很重要,因為未安裝,需要初始化自動實作的屬性必須伴隨的建構函式。如此一來,指導方針:「 請考慮在欄位的自動實作的屬性 」 (即使私用欄位) 會有意義,同時,您便無法再因為程式碼變得更精簡修改其包含的屬性以外的欄位。這些全部偏好另一個的指導方針,"請避免存取外部及其包含的屬性,從欄位 」 強調先前所述的資料封裝 (encapsulation) 原則,甚至是從其他類別成員。

此時,可讓返回 [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 tuple 從此以後稱為 tuple 不是整個物件 (例如個人或 CardDeck 物件) 的相關。相反地,它是基於運輸,任意分組,因此無法從沒有使用 out 或 ref 參數的麻煩的方法會傳回個別的組件有關。Mads Torgersen 使用一堆恰巧相同的匯流排的人員,其中匯流排就像是 tuple,而人就像是在 tuple 中的項目。項目中群組在一起傳回的 tuple 參數因為它們所有送往傳回給呼叫者,就不會報告它們一定會有任何其他彼此的關聯。事實上,很可能呼叫端會再 tuple 中擷取值,並處理這些個別而不是做為一個單位。

個別的項目,而不是整個重要性可讓封裝 (encapsulation) 概念較少吸引人。假設在 tuple 中的項目可以是彼此完全無關,就通常不需要變更 Item1,例如,可能會影響 Item2 以此方式封裝。(相較之下,變更弧形的長度會需要變更一個或同時角度讓封裝 (encapsulation) 是必備)。 此外,也沒有無效的值儲存在 tuple 內的項目。會強制執行任何驗證,而非在其中一個 tuple 的項目屬性的指派中的項目本身的資料類型。

基於這個理由,tuple 上的屬性沒有提供任何值,而且沒有任何傳入的未來值,它們無法提供。簡單地說,如果您要定義的類型的資料都不需要驗證的可變動,您也可以使用欄位。您可能想要利用屬性的另一個原因是具有不同的存取範圍之間的 getter 和 setter。不過,假設可接受的可變動性,您不打算利用屬性具有不同的 getter/setter family-or-assembly 存取範圍,或是。這些都會引發另一個問題,應該元組型別是可變動?

未定義可變動的實值類型

下一步的指引,請考慮是可變動的值類型。同樣地,弧線範例 (在程式碼所示圖 2) 違反指導方針。如果您認為資訊,請參閱很明顯,實值類型傳遞複本,因此變更此複本將無法從呼叫的可觀察。但是,當中的程式碼圖 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);
  }
}

請注意,引動過程弧線旋轉函式,即便在弧線事實上,永遠不會旋轉。為什麼會這樣?這會造成混淆的行為是因為兩個因素的組合。首先,弧線不會使它的值,而不是由參考傳遞實值類型。如此一來,叫用圓形圖。弧線傳回弧線,而是傳回未具現化的弧形的相同執行個體建構函式中的複本。這就不會有問題,如果它不是第二個因素。旋轉的引動過程要修改儲存在圓形圖內弧線的執行個體,但事實上,它會修改弧線屬性傳回的複本。而這就是為什麼我們有指導方針,「 不會定義可變動的實值類型。 」

為之前,在 C# 7.0 中的 tuple 略過這項指導方針,並公開公用的欄位,根據定義,請 ValueTuple <> … 可變動。儘管此違規,ValueTuple <>...不會發生相同的缺點為弧線。原因是修改 tuple 的唯一方式是透過項目欄位。不過,C# 編譯器不允許修改包含的類型 (包含類型為參考類型、 實值型別或甚至陣列或集合的其他型別) 所傳回的欄位 (或屬性)。例如,將不會編譯下列程式碼:

pie.Arc.Radius = 0;

也不將此程式碼:

pie.Arc.Radius++;

這些陳述式失敗並出現訊息中,「 錯誤 CS1612:無法修改 'PieShape.Arc' 的傳回值不是變數,因為。 」 換句話說,不一定是正確的指導方針。而不是避免所有可變動的值類型,關鍵在於避免變更函式 (讀取/寫入屬性所允許的)。常識,當然,假設所示的實值語意圖 2會不夠明顯,使得預期的內建實值型別行為。

請勿建立實值型別大於 16 個位元組

由於實值型別複製的頻率,則需要這個指導方針。事實上,除了 ref 或 out 參數,實值型別是幾乎每次複製要存取它們。指派到另一個實值類型執行個體是否為 true (例如弧線 = 在弧線圖 3) 或方法引動過程 (例如 Modify(arc) 示圖 2)。基於效能考量,是保持值的類型大小的指導方針。

但實際上是 ValueTuple <>...可能的大小通常超過 128 位元 (16 個位元組) 因為 ValueTuple <>...可能包含七個個別項目 (以及更多如果您指定另一個 tuple 的第八個型別參數)。原因,然後 C# 7.0 tuple 定義為實值類型嗎?

如先前所述,tuple 引進可多個傳回值而不需要複雜的語法所需的語言功能為 out 或 ref 參數。一般模式,然後是建構及傳回 tuple,然後將它分解回在呼叫端。事實上,透過傳回參數傳遞堆疊的深處 tuple 是類似於傳遞的引數的方法呼叫堆疊中的群組。換句話說,傳回的 tuple 是對稱的個別的參數清單,就記憶體而言。

如果宣告為參考類型,tuple,那麼就必須建構在堆積上的型別,並將它初始化的項目值 — 可能複製值或堆積的參考。無論如何,記憶體複製作業是必要的、 類似的實值類型的記憶體內部複本。此外,當參考 tuple 無法再存取時的時間之後,記憶體回收行程必須復原的記憶體。換句話說,參考 tuple 仍牽涉到記憶體複製,以及額外的壓力,記憶體回收行程,讓值型別 tuple 更有效率的選項。(在少數情況下的值 tuple 不是更有效率,就無法仍求助於參考類型的版本,Tuple <> …。)

雖然完全正交發行項的主要主題,請注意 Equals 和 GetHashCode,在實作圖 1。您可以看到 tuple 實作 Equals 和 GetHashCode 所提供的捷徑。如需詳細資訊,請參閱 「"使用 Tuple 來覆寫等號和 GetHashCode。

總結

乍看之下可能顯得驚人的 tuple,必須定義為不可變的實值類型。在所有情況下,.NET Core 和.NET Framework 中找到的不可變的實值類型的數目很少,而且沒有屆時程式設計呼叫實值型別為不可變的且封裝使用屬性的指導方針。也是不可變的預設方法特性至 F #、 pressured C# 語言設計工具,可提供宣告不可變的變數,或定義不可變的類型縮寫的影響。(雖然這類的縮寫目前正在進行考量適用於 C# 8.0,唯讀結構已加入 C# 7.2 做為驗證結構不可變)。

不過,當您深入瞭解詳細資料,您會看到一些重要因素。這些工作包括:

• 參考型別會造成記憶體回收與其他效能影響。

• Tuple 是通常是暫時性的。

• Tuple 項目不需要可預見屬性的封裝。

• 很大 (藉由值類型的指導方針) 的甚至 tuple 沒有很大的記憶體超過該參考 tuple 實作的複製作業。

在 [摘要] 有充分的偏好即便標準指導方針的公用欄位的值型別 tuple 的因素。最後,指導方針是,指導方針。不會忽略它們,但提供足夠 — 和建議,明確地記載 — 原因,它是 [確定],在某些情況下之外的線條色彩。

如需有關定義實值類型和覆寫 Equals 和 GetHashCode 的指導方針的詳細資訊,請參閱 9 和 10 我基本 C# 活頁簿中的章節:< 基本 C# 7.0 > (IntelliTect.com/EssentialCSharp),這必須是出在五月。


標記 Michaelis 是創辦 IntelliTect,他作為其主要技術架構設計人員和訓練。幾乎二十他 Microsoft MVP,同時也已自 2007 Microsoft 地區導向器。Michaelis 做數個 Microsoft 軟體設計檢閱小組,包括 C#、 Mi crosoft Azure、 SharePoint 和 Visual Studio ALM。他在開發人員所做的心得及已撰寫許多包括他 most recent、"Es sential C# 6.0 (第 5 版) 」 (itl.tc/EssentialCSharp)。在 Facebook 上連絡他facebook.com/Mark.Michaelis,在他的部落格上IntelliTect.com/Mark,Twitter 上: @markmichaelis或透過電子郵件在mark@IntelliTect.com