乡下人产国偷v产偷v自拍,国产午夜片在线观看,婷婷成人亚洲综合国产麻豆,久久综合给合久久狠狠狠9

  • <output id="e9wm2"></output>
    <s id="e9wm2"><nobr id="e9wm2"><ins id="e9wm2"></ins></nobr></s>

    • 分享

      新版 C# 高效率編程指南

       行者花雕 2021-03-16

      前言

      C# 從 7 版本開始一直到如今的 9 版本,加入了非常多的特性,其中不乏改善性能、增加程序健壯性和代碼簡潔性、可讀性的改進,這里我整理一些使用新版 C# 的時候個人推薦的寫法,可能不適用于所有的人,但是還是希望對你們有所幫助。

      注意:本指南適用于 .NET 5 或以上版本。

      使用 ref struct 做到 0 GC

      C# 7 開始引入了一種叫做 ref struct 的結構,這種結構本質是 struct ,結構存儲在棧內存。但是與 struct 不同的是,該結構不允許實現(xiàn)任何接口,并由編譯器保證該結構永遠不會被裝箱,因此不會給 GC 帶來任何的壓力。相對的,使用中就會有不能逃逸出棧的強制限制。

      Span<T> 就是利用 ref struct 的產物,成功的封裝出了安全且高性能的內存訪問操作,且可在大多數(shù)情況下代替指針而不損失任何的性能。

      ref struct MyStruct
      {
          public int Value { get; set; }
      }
          
      class RefStructGuide
      {
          static void Test()
          {
              MyStruct x = new MyStruct();
              x.Value = 100;
              Foo(x); // ok
              Bar(x); // error, x cannot be boxed
          }
      
          static void Foo(MyStruct x) { }
          
          static void Bar(object x) { }
      }
      

      使用 in 關鍵字傳遞不可修改的引用

      當參數(shù)以 ref 傳遞時,雖然傳遞的是引用但是無法確保引用值不被對方修改,這個時候只需要將 ref 改為 in,便能確保安全性:

      SomeBigReadonlyStruct x = ...;
      Foo(x);
      
      void Foo(in SomeBigReadonlyStruct v)
      {
          v = ...; // error
      }
      

      在使用大的 readonly struct 時收益非常明顯。

      使用 stackalloc 在棧上分配連續(xù)內存

      對于部分性能敏感卻需要使用少量的連續(xù)內存的情況,不必使用數(shù)組,而可以通過 stackalloc 直接在棧上分配內存,并使用 Span<T> 來安全的訪問,同樣的,這么做可以做到 0 GC 壓力。

      stackalloc 允許任何的值類型結構,但是要注意,Span<T> 目前不支持 ref struct 作為泛型參數(shù),因此在使用 ref struct 時需要直接使用指針。

      ref struct MyStruct
      {
          public int Value { get; set; }
      }
      
      class AllocGuide
      {
          static unsafe void RefStructAlloc()
          {
              MyStruct* x = stackalloc MyStruct[10];
              for (int i = 0; i < 10; i++)
              {
                  *(x + i) = new MyStruct { Value = i };
              }
          }
      
          static void StructAlloc()
          {
              Span<int> x = stackalloc int[10];
              for (int i = 0; i < x.Length; i++)
              {
                  x[i] = i;
              }
          }
      }
      

      使用 Span 操作連續(xù)內存

      C# 7 開始引入了 Span<T>,它封裝了一種安全且高性能的內存訪問操作方法,可用于在大多數(shù)情況下代替指針操作。

      static void SpanTest()
      {
          Span<int> x = stackalloc int[10];
          for (int i = 0; i < x.Length; i++)
          {
              x[i] = i;
          }
      
          ReadOnlySpan<char> str = "12345".AsSpan();
          for (int i = 0; i < str.Length; i++)
          {
              Console.WriteLine(str[i]);
          }
      }
      

      性能敏感時對于頻繁調用的函數(shù)使用 SkipLocalsInit

      C# 為了確保代碼的安全會將所有的局部變量在聲明時就進行初始化,無論是否必要。一般情況下這對性能并沒有太大影響,但是如果你的函數(shù)在操作很多棧上分配的內存,并且該函數(shù)還是被頻繁調用的,那么這一消耗的副作用將會被放大變成不可忽略的損失。

      因此你可以使用 SkipLocalsInit 這一特性禁用自動初始化局部變量的行為。

      [SkipLocalsInit]
      unsafe static void Main()
      {
          Guid g;
          Console.WriteLine(*&g);
      }
      

      上述代碼將輸出不可預期的結果,因為 g 并沒有被初始化為 0。另外,訪問未初始化的變量需要在 unsafe 上下文中使用指針進行訪問。

      使用函數(shù)指針代替 Marshal 進行互操作

      C# 9 帶來了函數(shù)指針功能,該特性支持 managed 和 unmanaged 的函數(shù),在進行 native interop 時,使用函數(shù)指針將能顯著改善性能。

      例如,你有如下 C++ 代碼:

      #define UNICODE
      #define WIN32
      #include <cstring>
      
      extern "C" __declspec(dllexport) char* __cdecl InvokeFun(char* (*foo)(int)) {
          return foo(5);
      }
      

      并且你編寫了如下 C# 代碼進行互操作:

      [DllImport("./Test.dll")]
      static extern string InvokeFun(delegate* unmanaged[Cdecl]<int, IntPtr> fun);
      
      [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]
      public static IntPtr Foo(int x)
      {
          var str = Enumerable.Repeat("x", x).Aggregate((a, b) => $"{a}");
          return Marshal.StringToHGlobalAnsi(str);
      }
      
      static void Main(string[] args)
      {
          var callback = (delegate* unmanaged[Cdecl]<int, nint>)(delegate*<int, nint>)&Foo;
          Console.WriteLine(InvokeFun(callback));
      }
      

      上述代碼中,首先 C# 將自己的 Foo 方法作為函數(shù)指針傳給了 C++ 的 InvokeFun 函數(shù),然后 C++ 用參數(shù) 5 調用該函數(shù)并返回其返回值到 C# 的調用方。

      注意到上述代碼還用了 UnmanagedCallersOnly 這一特性,這樣可以告訴編譯器該方法只會從 unmanaged 的代碼被調用,因此編譯器可以做一些額外的優(yōu)化。

      使用函數(shù)指針產生的 IL 指令非常高效:

      ldftn native int Test.Program::Foo(int32)
      stloc.0
      ldloc.0
      call string Test.Program::InvokeFun(method native int *(int32))
      

      除了 unmanaged 的情況外,managed 函數(shù)也是可以使用函數(shù)指針的:

      static void Foo(int v) { }
      unsafe static void Main(string[] args)
      {
          delegate* managed<int, void> fun = &Foo;
          fun(4);
      }
      

      產生的代碼相對于原本的 Delegate 來說更加高效:

      ldftn void Test.Program::Foo(int32)
      stloc.0
      ldc.i4.4
      ldloc.0
      calli void(int32)
      

      使用模式匹配

      有了if-else、as和強制類型轉換,為什么要使用模式匹配呢?有三方面原因:性能、魯棒性和可讀性。

      為什么說性能也是一個原因呢?因為 C# 編譯器會根據(jù)你的模式編譯出最優(yōu)的匹配路徑。

      考慮一下以下代碼(代碼 1):

      int Match(int v)
      {
          if (v > 3)
          {
              return 5;
          }
          if (v < 3)
          {
              if (v > 1)
              {
                  return 6;
              }
              if (v > -5)
              {
                  return 7;
              }
              else
              {
                  return 8;
              }
          }
          return 9;
      }
      

      如果改用模式匹配,配合 switch 表達式寫法則變成(代碼 2):

      int Match(int v)
      {
          return v switch
          {
              > 3 => 5,
              < 3 and > 1 => 6,
              < 3 and > -5 => 7,
              < 3 => 8,
              _ => 9
          };
      }
      

      以上代碼會被編譯器編譯為:

      int Match(int v)
      {
          if (v > 1)
          {
              if (v <= 3)
              {
                  if (v < 3)
                  {
                      return 6;
                  }
                  return 9;
              }
              return 5;
          }
          if (v > -5)
          {
              return 7;
          }
          return 8;
      }
      

      我們計算一下平均比較次數(shù):

      代碼 5 6 7 8 9 總數(shù) 平均
      代碼 1 1 3 4 4 2 14 2.8
      代碼 2 2 3 2 2 3 12 2.4

      可以看到使用模式匹配時,編譯器選擇了更優(yōu)的比較方案,你在編寫的時候無需考慮如何組織判斷語句,心智負擔降低,并且代碼 2 可讀性和簡潔程度顯然比代碼 1 更好,有哪些條件分支一目了然。

      甚至遇到類似以下的情況時:

      int Match(int v)
      {
          return v switch
          {
              1 => 5,
              2 => 6,
              3 => 7,
              4 => 8,
              _ => 9
          };
      }
      

      編譯器會直接將代碼從條件判斷語句編譯成 switch 語句:

      int Match(int v)
      {
          switch (v)
          {
              case 1:
                  return 5;
              case 2:
                  return 6;
              case 3:
                  return 7;
              case 4:
                  return 8;
              default:
                  return 9;
          }
      }
      

      如此一來所有的判斷都不需要比較(因為 switch 可根據(jù) HashCode 直接跳轉)。

      編譯器非常智能地為你選擇了最佳的方案。

      那魯棒性從何談起呢?假設你漏掉了一個分支:

      int v = 5;
      var x = v switch
      {
          > 3 => 1,
          < 3 => 2
      };
      

      此時編譯的話,編譯器就會警告你漏掉了 v 可能為 3 的情況,幫助減少程序出錯的可能性。

      最后一點,可讀性。

      假設你現(xiàn)在有這樣的東西:

      abstract class Entry { }
      
      class UserEntry : Entry
      {
          public int UserId { get; set; }
      }
      
      class DataEntry : Entry
      {
          public int DataId { get; set; }
      }
      
      class EventEntry : Entry
      {
          public int EventId { get; set; }
          // 如果 CanRead 為 false 則查詢的時候直接返回空字符串
          public bool CanRead { get; set; }
      }
      

      現(xiàn)在有接收類型為 Entry 的參數(shù)的一個函數(shù),該函數(shù)根據(jù)不同類型的 Entry 去數(shù)據(jù)庫查詢對應的 Content,那么只需要寫:

      string QueryMessage(Entry entry)
      {
          return entry switch
          {
              UserEntry u => dbContext1.User.FirstOrDefault(i => i.Id == u.UserId).Content,
              DataEntry d => dbContext1.Data.FirstOrDefault(i => i.Id == d.DataId).Content,
              EventEntry { EventId: var eventId, CanRead: true } => dbContext1.Event.FirstOrDefault(i => i.Id == eventId).Content,
              EventEntry { CanRead: false } => "",
              _ => throw new InvalidArgumentException("無效的參數(shù)")
          };
      }
      

      更進一步,假如 Entry.Id 分布在了數(shù)據(jù)庫 1 和 2 中,如果在數(shù)據(jù)庫 1 當中找不到則需要去數(shù)據(jù)庫 2 進行查詢,如果 2 也找不到才返回空字符串,由于 C# 的模式匹配支持遞歸模式,因此只需要這樣寫:

      string QueryMessage(Entry entry)
      {
          return entry switch
          {
              UserEntry u => dbContext1.User.FirstOrDefault(i => i.Id == u.UserId) switch
              {
                  null => dbContext2.User.FirstOrDefault(i => i.Id == u.UserId)?.Content ?? "",
                  var found => found.Content
              },
              DataEntry d => dbContext1.Data.FirstOrDefault(i => i.Id == d.DataId) switch
              {
                  null => dbContext2.Data.FirstOrDefault(i => i.Id == u.DataId)?.Content ?? "",
                  var found => found.Content
              },
              EventEntry { EventId: var eventId, CanRead: true } => dbContext1.Event.FirstOrDefault(i => i.Id == eventId) switch
              {
                  null => dbContext2.Event.FirstOrDefault(i => i.Id == eventId)?.Content ?? "",
                  var found => found.Content
              },
              EventEntry { CanRead: false } => "",
              _ => throw new InvalidArgumentException("無效的參數(shù)")
          };
      }
      

      就全部搞定了,代碼非常簡潔,而且數(shù)據(jù)的流向一眼就能看清楚,就算是沒有接觸過這部分代碼的人看一下模式匹配的過程,也能一眼就立刻掌握各分支的情況,而不需要在一堆的 if-else 當中梳理這段代碼到底干了什么。

      使用記錄類型和不可變數(shù)據(jù)

      record 作為 C# 9 的新工具,配合 init 僅可初始化屬性,為我們帶來了高效的數(shù)據(jù)交互能力和不可變性。

      消除可變性意味著無副作用,一個無副作用的函數(shù)無需擔心數(shù)據(jù)同步互斥問題,因此在無鎖的并行編程中非常有用。

      record Point(int X, int Y);
      

      簡單的一句話等價于我們寫了如下代碼,幫我們解決了 ToString() 格式化輸出、基于值的 GetHashCode() 和相等判斷等等各種問題:

      internal class Point : IEquatable<Point>
      {
          private readonly int x;
          private readonly int y;
      
          protected virtual Type EqualityContract => typeof(Point);
      
          public int X
          {
              get => x;
              set => x = value;
          }
      
          public int Y
          {
              get => y;
              set => y = value;
          }
      
          public Point(int X, int Y)
          {
              x = X;
              y = Y;
          }
      
          public override string ToString()
          {
              StringBuilder stringBuilder = new StringBuilder();
              stringBuilder.Append("Point");
              stringBuilder.Append(" { ");
              if (PrintMembers(stringBuilder))
              {
                  stringBuilder.Append(" ");
              }
              stringBuilder.Append("}");
              return stringBuilder.ToString();
          }
      
          protected virtual bool PrintMembers(StringBuilder builder)
          {
              builder.Append("X");
              builder.Append(" = ");
              builder.Append(X.ToString());
              builder.Append(", ");
              builder.Append("Y");
              builder.Append(" = ");
              builder.Append(Y.ToString());
              return true;
          }
      
          public static bool operator !=(Point r1, Point r2)
          {
              return !(r1 == r2);
          }
      
          public static bool operator ==(Point r1, Point r2)
          {
              if ((object)r1 != r2)
              {
                  if ((object)r1 != null)
                  {
                      return r1.Equals(r2);
                  }
                  return false;
              }
              return true;
          }
      
          public override int GetHashCode()
          {
              return (EqualityComparer<Type>.Default.GetHashCode(EqualityContract) * -1521134295 + EqualityComparer<int>.Default.GetHashCode(x)) * -1521134295 + EqualityComparer<int>.Default.GetHashCode(y);
          }
      
          public override bool Equals(object obj)
          {
              return Equals(obj as Point);
          }
      
          public virtual bool Equals(Point other)
          {
              if ((object)other != null && EqualityContract == other.EqualityContract && EqualityComparer<int>.Default.Equals(x, other.x))
              {
                  return EqualityComparer<int>.Default.Equals(y, other.y);
              }
              return false;
          }
      
          public virtual Point Clone()
          {
              return new Point(this);
          }
      
          protected Point(Point original)
          {
              x = original.x;
              y = original.y;
          }
      
          public void Deconstruct(out int X, out int Y)
          {
              X = this.X;
              Y = this.Y;
          }
      }
      

      注意到 xy 都是 readonly 的,因此一旦實例創(chuàng)建了就不可變,如果想要變更可以通過 with 創(chuàng)建一份副本,于是這種方式徹底消除了任何的副作用。

      var p1 = new Point(1, 2);
      var p2 = p1 with { Y = 3 }; // (1, 3)
      

      當然,你也可以自己使用 init 屬性表示這個屬性只能在初始化時被賦值:

      class Point
      {
          public int X { get; init; }
          public int Y { get; init; }
      }
      

      這樣一來,一旦 Point 被創(chuàng)建,則 XY 的值就不會被修改了,可以放心地在并行編程模型中使用,而不需要加鎖。

      var p1 = new Point { X = 1, Y = 2 };
      p1.Y = 3; // error
      var p2 = p1 with { Y = 3 }; //ok
      

      使用 readonly 類型

      上面說到了不可變性的重要性,當然,struct 也可以是只讀的:

      readonly struct Foo
      {
          public int X { get; set; } // error
      }
      

      上面的代碼會報錯,因為違反了 X 只讀的約束。

      如果改成:

      readonly struct Foo
      {
          public int X { get; }
      }
      

      readonly struct Foo
      {
          public int X { get; init; }
      }
      

      則不會存在問題。

      Span<T> 本身是一個 readonly ref struct,通過這樣做保證了 Span<T> 里的東西不會被意外的修改,確保不變性和安全。

      使用局部函數(shù)而不是 lambda 創(chuàng)建臨時委托

      在使用 Expression<Func<>> 作為參數(shù)的 API 時,使用 lambda 表達式是非常正確的,因為編譯器會把我們寫的 lambda 表達式編譯成 Expression Tree,而非直觀上的函數(shù)委托。

      而在單純只是 Func<>、Action<> 時,使用 lambda 表達式恐怕不是一個好的決定,因為這樣做必定會引入一個新的閉包,造成額外的開銷和 GC 壓力。從 C# 8 開始,我們可以使用局部函數(shù)很好的替換掉 lambda:

      int SomeMethod(Func<int, int> fun)
      {
          if (fun(3) > 3) return 3;
          else return fun(5);
      }
      
      void Caller()
      {
          int Foo(int v) => v + 1;
      
          var result = SomeMethod(Foo);
          Console.WriteLine(result);
      }
      

      以上代碼便不會導致一個多余的閉包開銷。

      使用 ValueTask 代替 Task

      我們在遇到 Task<T> 時,大多數(shù)情況下只是需要簡單的對其進行 await 而已,而并不需要將其保存下來以后再 await,那么 Task<T> 提供的很多的功能則并沒有被使用,反而在高并發(fā)下,由于反復分配 Task 導致 GC 壓力增加。

      這種情況下,我們可以使用 ValueTask<T> 代替 Task<T>

      ValueTask<int> Foo()
      {
          return ValueTask.FromResult(1);
      }
      
      async ValueTask Caller()
      {
          await Foo();
      }
      

      由于 ValueTask<T> 是值類型結構,因此該對象本身不會在堆上分配內存,于是可以減輕 GC 壓力。

      實現(xiàn)解構函數(shù)代替創(chuàng)建元組

      如果我們想要把一個類型中的數(shù)據(jù)提取出來,我們可以選擇返回一個元組,其中包含我們需要的數(shù)據(jù):

      class Foo
      {
          private int x;
          private int y;
      
          public Foo(int x, int y)
          {
              this.x = x;
              this.y = y;
          }
      
          public (int, int) Deconstruct()
          {
              return (x, y);
          }
      }
      
      class Program
      {
          static void Bar(Foo v)
          {
              var (x, y) = v.Deconstruct();
              Console.WriteLine($"X = {x}, Y = {y}");
          }
      }
      

      上述代碼會導致一個 ValueTuple<int, int> 的開銷,如果我們將代碼改成實現(xiàn)解構方法:

      class Foo
      {
          private int x;
          private int y;
      
          public Foo(int x, int y)
          {
              this.x = x;
              this.y = y;
          }
      
          public void Deconstruct(out int x, out int y)
          {
              x = this.x;
              y = this.y;
          }
      }
      
      class Program
      {
          static void Bar(Foo v)
          {
              var (x, y) = v;
              Console.WriteLine($"X = {x}, Y = {y}");
          }
      }
      

      則不僅省掉了 Deconstruct() 的調用,同時還沒有任何的額外開銷。你可以看到實現(xiàn) Deconstruct 函數(shù)并不需要讓你的類型實現(xiàn)任何的接口,從根本上杜絕了裝箱的可能性,這是一種 0 開銷抽象。另外,解構函數(shù)還能用于做模式匹配,你可以像使用元組一樣地使用解構函數(shù)(下面代碼的意思是,當 x 為 3 時取 y,否則取 x + y):

      void Bar(Foo v)
      {
          var result = v switch
          {
              Foo (3, var y) => y,
              Foo (var x, var y) => x + y,
              _ => 0
          };
      
          Console.WriteLine(result);
      }
      

      Null 安全

      在項目屬性文件 csproj 中啟用 null 安全后即可對整個項目的代碼啟用 null 安全靜態(tài)分析:

      <PropertyGroup>
          <Nullable>enable</Nullable>
      </PropertyGroup>
      

      這樣便可以在編譯的時候檢查一切潛在的導致 NRE 的問題。例如如下代碼:

      var list = new List<Entry>();
      var value = list.FirstOrDefault(i => i.Id == 3).Value;
      Console.WriteLine(value);
      

      list.FirstOrDefault() 可能返回 null,因此啟用 null 安全之后編譯器將會給出警告,這有助于避免不必要的 NRE 異常發(fā)生。

      另外,啟用 null 安全之后,對于可空引用類型,也可以通過在類型后加一個 ? 來表示可為 null

      string? x = null;
      

      總結

      在合適的時候使用 C# 的新特性,不但可以提升開發(fā)效率,同時還能兼顧代碼質量和運行效率的提升。

      但是切忌濫用。新特性的引入對于我們寫高質量的代碼無疑有很大的幫助,但是如果不分時宜地使用,可能會帶來反效果。

      希望本文能對各位開發(fā)者使用新版 C# 時帶來一定的幫助,感謝閱讀。

        本站是提供個人知識管理的網絡存儲空間,所有內容均由用戶發(fā)布,不代表本站觀點。請注意甄別內容中的聯(lián)系方式、誘導購買等信息,謹防詐騙。如發(fā)現(xiàn)有害或侵權內容,請點擊一鍵舉報。
        轉藏 分享 獻花(0

        0條評論

        發(fā)表

        請遵守用戶 評論公約

        類似文章 更多