Tuple VS ValueTuple
為什么有此文章
首先要說的是我們公司內部技術框架是用 abp.vnext 框架整合而來的,我們架構師對于 abp 相關的知識都很了然于胸了。并且這個框架的確很優秀,省了我們前期大量基礎工作。架構師把主要的架子搭建好了之后,把應用層與核心層讓我們去實現,并讓我們好熟悉這個框架。
就在我們在討論代碼規范相關的東西,就說到了值元祖這個點,并提議不要在代碼中用元組。我當時聽了之后覺得疑惑,為什么不能用元組呢?元組的確很方便啊,特別是 C#7.0 之后支持元組解構,代碼閱讀性,美觀度雙雙提升。他是說元組在取值的時候會發生裝箱,會有性能損耗。再者值元組跟之前的 Tuple 不同,前者是一個結構體,后者則是引用類型,在用值元組的時候會不利于垃圾回收(具體是說 Ioc 管理的生命周期與我在用的值元組變量的生命周期會有矛盾)。
在最開始的話,我并沒有這么考慮,因為我心里想著是這樣的:
Tuple<T>?和?ValueTuple<T>?是泛型的,是不會發生裝箱的(這點在我查看了源代碼以及 IL 發現很有意思,后面會有提到)
ValueTuple<T>?是值對象沒錯,內存分配在棧中,但還是屬于托管資源,CLR 會管理好每個變量的生命周期的,會確保值類型變量在當前作用域無任何引用時會釋放資源。比如我在程序中是新建的局部變量,那么哪怕是這個變量未引用,已經引用過后再無引用,CLR 都會自動回收這個局部變量。
而后我查看?Tuple?和?ValueTuple?的api,心情可謂是一波三折啊。所以在有此文章
ValueTuple
先來看 ValueTuple,查看其成員信息如下:
public struct ValueTuple : IStructuralComparable, IStructuralEquatable, IComparable, IComparable<ValueTuple>, IEquatable<ValueTuple>, ITuple這里面有一個成員信息特別扎眼,那就是?ITuple?類,因為其他的接口都是跟判斷相等性相關的。不在我們這次的討論范圍。
我們 F12 到 ITuple 進去看看具體成員信息
object? this[int? index] { get; }int? Length { get; }不過我們在暴露出來的 API 中沒有看到這兩個實現屬性,說明這個實現類的這這兩個屬性只是內部實現用的,不會給我們開發者用(當然,我們可以選擇強轉來使用接口的這兩個屬性)。這從源碼也是可以很容易知道的:
[Nullable(2)] object ITuple.this[int index] {get{if(index != 0){throw new IndexOutOfRangeException();}return Item1;} } int ITuple.Length {get{return 1;} }當我在官網看到 ValueTuple 有兩個屬性是實現接口 ITuple 的,并且?ITuple.Item[Int32]?返回的是一個?object?對象,我下意識的反映就是難道真的會發生裝箱么?仔細想想其實完全不是這樣,如果是發生裝箱的話,那么這個 ValueTuple 泛型就是一個多余的東西,那就跟 java 中的泛型擦除效果一樣了,只是起到了一個編譯期的檢測作用,不能做到實質的性能提升。
其實再仔細查看便會發現,我們平常引用的 ValueTuple 、Tuple 實例對象引用的 Item1、Item2 等值實際上是字段而不是屬性,而這些字段在你初始化或用?Tuple.Creat,ValueTuple.Create?函數創建的元組 / 值元組對象時,類型以及 Item 的個數以及值就已經確定了。所以根本不會發生裝箱。這一點我們從 IL 代碼中就能從中得知
在看 IL 之前我們先來看與 IL 對應的 C# 代碼
var t = ValueTuple.Create(2, 3); Console.WriteLine(t.Item1); Console.WriteLine(t.Item2); Console.WriteLine($"Item1 = {t.Item1}, Item2= ${t.Item2}");IL 代碼:
.method private hidebysig static void Main (string[] args) cil managed {.maxstack 3.entrypoint.locals init ([0] valuetype [System.Runtime]System.ValueTuple`2<int32, int32> t)IL_0000: nopIL_0001: ldc.i4.2IL_0002: ldc.i4.3IL_0003: call valuetype [System.Runtime]System.ValueTuple`2<!!0, !!1> [System.Runtime]System.ValueTuple::Create<int32, int32>(!!0, !!1)IL_0008: stloc.0IL_0009: ldloc.0IL_000a: ldfld !0 valuetype [System.Runtime]System.ValueTuple`2<int32, int32>::Item1IL_000f: call void [System.Console]System.Console::WriteLine(int32)IL_0014: nopIL_0015: ldloc.0IL_0016: ldfld !1 valuetype [System.Runtime]System.ValueTuple`2<int32, int32>::Item2IL_001b: call void [System.Console]System.Console::WriteLine(int32)IL_0020: nopIL_0021: ldstr "Item1 = {0}, Item2= ${1}"IL_0026: ldloc.0IL_0027: ldfld !0 valuetype [System.Runtime]System.ValueTuple`2<int32, int32>::Item1IL_002c: box [System.Runtime]System.Int32IL_0031: ldloc.0IL_0032: ldfld !1 valuetype [System.Runtime]System.ValueTuple`2<int32, int32>::Item2IL_0037: box [System.Runtime]System.Int32IL_003c: call string [System.Runtime]System.String::Format(string, object, object)IL_0041: call void [System.Console]System.Console::WriteLine(string)IL_0046: nopIL_0047: ret }這樣我們就能很清楚的知道元組里面的細節了,我們平常取的都是元組 / 值元組的字段,并且 Main 函數開頭的?managed?標識就代表這是托管資源。值得注意的是 IL_002c 處的裝箱只是由于 Console.WriteLine 導致的裝箱。
元組解構
我們知道 C#7 支持了元組結構了,可以支持我們對元組字段 Item 進行表意話,這樣更能提高閱讀性和代碼美觀。那么元組結構跟之前直接引用的字段值變量 Item 有什么區別呢?這一點我們也可以直接從 IL 上輕易得知。
var (pd, id) = ValueTuples.Create(2, 3); Console.WriteLine(pd); Console.WriteLine(id); Console.WriteLine($"元組解構:Item1 = {pd}, Item2= ${id}");.method private hidebysig static void Main (string[] args) cil managed {.maxstack 3.entrypoint.locals init ([0] int32 pd,[1] int32 id)IL_0000: nopIL_0001: ldc.i4.2IL_0002: ldc.i4.3IL_0003: call valuetype [System.Runtime]System.ValueTuple`2<!!0, !!0> CSharpGuide.LanguageVersions._7._0.ValueTuples::Create<int32>(!!0, !!0)IL_0008: dupIL_0009: ldfld !0 valuetype [System.Runtime]System.ValueTuple`2<int32, int32>::Item1IL_000e: stloc.0IL_000f: ldfld !1 valuetype [System.Runtime]System.ValueTuple`2<int32, int32>::Item2IL_0014: stloc.1IL_0015: ldloc.0IL_0016: call void [System.Console]System.Console::WriteLine(int32)IL_001b: nopIL_001c: ldloc.1IL_001d: call void [System.Console]System.Console::WriteLine(int32)IL_0022: nopIL_0023: ldstr "元組解構:Item1 = {0}, Item2= ${1}"IL_0028: ldloc.0IL_0029: box [System.Runtime]System.Int32IL_002e: ldloc.1IL_002f: box [System.Runtime]System.Int32IL_0034: call string [System.Runtime]System.String::Format(string, object, object)IL_0039: call void [System.Console]System.Console::WriteLine(string)IL_003e: nopIL_003f: ret }發現了沒有,這段 IL 與之前的一模一樣,沒任何區別。
ITuple
如果你想用把元組轉換成?ITuple?類型,那么取的值就一定會發生裝箱,因為 Item 是一個?object?類型。我們能從這個類獲知這個元組有多少個值,能通過索引遍歷所有的值。除此之外,這個類并沒有其他使用場景了。
Tuple、ValueTuple?平常的使用完全不用擔心 Item 值的裝箱,因為根本不會發生裝箱拆箱。元組解構生成的代碼跟之前直接引用元組是沒任何區別的。只是編譯器增加這么一個功能,給 item 命名的功能而已。如果你想要遍歷這個元組對象的值的話,那么就建議轉化成 ITuple 進一步操作。
文章同步至:https://github.com/MarsonShine/Books/blob/master/CSharpGuide/docs/7.0/TupleVSValueTuple.md
總結
以上是生活随笔為你收集整理的Tuple VS ValueTuple的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 使用.NET Core创建Windows
- 下一篇: 关于.NET HttpClient方式获