c#知识点

记录自己在学习c#遇到的知识点(容易忽略容易忘记得,或一些小技巧)[持续更新]

前言: 在大部分应用情况下,"效率"并没有那么高的地位,灵活性更重要.在部分情况下,"灵活性"并没有那么高的地位,效率最重要.

  • using结构只是保证可以调用 cmd.Dispose() 方法 和 connection.Dispose() 方法.而且不需要再包裹一层try{}catch{}
  • get 传参后台可以设置参数类型,[FromUri] Entity param,
public HttpResponseMessage reportsExcel(HttpRequestMessage request,[FromUri] ExportParam param){

}
//其中ExportParam 类
    public class ExportParam : ReportSearchParam
    {
        public string title { get; set; }
        public string companyName { get; set; }
        public DateTime date { get; set; }
        public string[] tableTitles { get; set; }
    }

此时,参数可以传递数组,但是数组的在url上的写法应是{{server}}Export/reportsExcel?companyId=1&startDate=2018-1-1&endDate=2018-2-1&title=测试&companyName=公司&date=2017-1-18&tableTitles=1111&tableTitles=aaa,即,将数组参数重复写

  • CallerMemberName,CallerFilePath,CallerLineNumber
/// 
/// Writes an error level logging message.
/// 
/// The message to be written.
public void WriteError(object message,
[CallerMemberName] string memberName = "",//调用函数名称
[CallerFilePath] string sourceFilePath = "",//调用文件
[CallerLineNumber] int sourceLineNumber = 0 //调用行号)
{
    _log4Net.ErrorFormat("文件:{0} 行号:{1} 方法名:{2},消息:{3}", sourceFilePath, sourceLineNumber, memberName, message);
 }
  • SortedDictionary
    对一个Dictionary进行键排序可以直接用SortedDictionary
    SortedDictionary 泛型类是检索运算复杂度为 O(log n) 的二叉搜索树。 就这一点而言,它与 SortedList 泛型类相似。 这两个类具有相似的对象模型,并且都具有 O(log n) 的检索运算复杂度。
    这两个类的区别在于内存的使用以及插入和移除元素的速度:
    SortedList 使用的内存比 SortedDictionary 少,SortedDictionary 可对未排序的数据执行更快的插入和移除操作:
    它的时间复杂度为 O(log n),而 SortedList 为 O(n),如果使用排序数据一次性填充,SortedList比 SortedDictionary 快。
    每个键/值对都可以作为KeyValuePair 结构进行检索,或作为DictionaryEntry通过非泛型IDictionary接口进行检索。只要键用作 SortedDictionary 中的键,它们就必须是不可变的。
    SortedDictionary 中的每个键必须是唯一的。 键不能为 null,但是如果值类型 TValue 为引用类型,该值则可以为空。
    SortedDictionary 需要比较器实现来执行键比较。 可以使用一个接受 comparer 参数的构造函数来指定 IComparer 泛型接口的实现;
    如果不指定实现,则使用默认的泛型比较器 Comparer.Default。
    如果类型 TKey 实现 System.IComparable 泛型接口,则默认比较器使用该实现。
    对一个Dictionary进行值排序可以用LINQ:
Dictionary MyDictionary = new Dictionary();
MyDictionary = (from entry in MyDictionary 
    orderby entry.Value ascending select entry).ToDictionary(pair => pair.Key, pair => pair.Value);
///   
/// 获取随机数
///   
///   
private static string GetRandom()
{
    Random rd = new Random(DateTime.Now.Millisecond);
    int i= rd.Next(0,int.MaxValue);
    return i.ToString();
}
  • 获取时间戳
///   
/// 获取时间戳  
///   
///   
private static string GetTimeStamp()
{
    TimeSpan ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0);
    return Convert.ToInt64(ts.TotalMilliseconds).ToString();
}
  • webapi 常用status code
200: OK
201: Created, 创建了新的资源
204: 无内容 No Content, 例如删除成功
400: Bad Request, 指的是客户端的请求错误.
401: 未授权 Unauthorized.
403: 禁止操作 Forbidden. 验证成功, 但是没法访问相应的资源
404: Not Found 
409: 有冲突 Conflict.
500: Internal Server Error, 服务器发生了错误
  • 过滤器和中间件的区别
    中间件是应用程序级别的,它可以处理每个发送过来的请求;而过滤器是针对MVC的,它只会处理发往MVC的请求。

  • ASP.NET Core MVC的过滤器分为5类:

  1. 授权过滤器,它是第一个运行的,它的作用就是判断HTTP Context中的用户是否拥有当前请求的权限,如果用户没有权限,那么它就会“短路”管道。
  2. 资源过滤器,在授权过滤器后运行,在管道其它动作之前,和管道动作都结束后运行。它可以实现缓存或由于性能原因执行短路操作。它在实体绑定之前运行,所以它也可以对影响实体绑定。
  3. Action过滤器,它在Action方法调用之前和之后立即执行,它可以操作传进Action的参数和返回的结果。
  4. 异常过滤器,针对在写入响应Body之前发生的未处理的异常,它可以应用全局的策略,
  5. 结果过滤器,它可以在每个Action结果执行之前和之后运行代码,但也只是在Action方法无错误的成功完成后才可以执行。
ASP.NET Core MVC 过滤器
  • 反射 取值 赋值
       public object Get()
        {
            TestSourceClass user = new TestSourceClass();
            user.id = 1;
            user.loginName = "wwmin";
            user.userName = "w";
            TestTargetClass test = new TestTargetClass();
            List ptList = new List(user.GetType().GetProperties());
            List ptNameList = ptList.Select(p => p.Name).ToList();
            List testPtList = new List(test.GetType().GetProperties());

            testPtList.ForEach(info =>
            {
                if (ptNameList.Contains(info.Name))
                {
                    object value = user.GetType().GetProperty(info.Name).GetValue(user);
                    if (value != null)
                    {
                        info.SetValue(test, value, null);
                    }
                }
            });

            return Json(test);
        }

        public class TestTargetClass
        {
            public int id { get; set; }
            public string userName { get; set; }
        }
        public class TestSourceClass
        {
            public int id { get; set; }
            public string userName { get; set; }
            public string userMobile { get; set; }
            public int companyId { get; set; }
            public string loginName { get; set; }
        }
  • 正确操作字符串
    拼接字符串一定要考虑使用StringBuilder,默认长度为16,实际看情况设置.
    StringBuilder本质:是以非托管方式分配内存
    同时StringFormat方法内部也是使用StringBuilder进行字符串格式化.
  • 使用默认转型方法

类型的转换运算符: 每个类型内部都有一个方法(运算符),分为隐式转换和显示转换.
使用类型内置的Parse、TryParse、ToDouble、ToDateTime
使用帮助类提供的方法: System.Convert类、System.BitConverter类来进行类型的转换.
使用CLR支持的类型: 父类和子类之间的转换.

  • 区别对待强制转型与as和is

什么时候使用as
如果类型之间都上溯到了某个共同的基类,那么根据此基类进行的转型(即基类转型为子类本身)应该使用as。子类与子类之间的转型,则应该提供转换操作符,以便进行强制转型。
as操作符永远不会抛出异常,如果类型不匹配(被转换对象的运行时类型既不是所转换的目标类型,也不是其派生类型),或者转型的源对象为null,那么转型之后的值也为null。

什么时候使用is
as操作符有一个问题,即它不能操作基元类型. 如果涉及基元类型的算法,就需要通过is转型的类型来进行判断,已避免转型失败.

  • TryParse 比Parse好

因为安全.

  • 使用int?来确保值类型也可以为null
    基元类型为什么需要为null?考虑两个场景:
    1.数据库支持整数可为空.
    2.数据在传输过程中存在丢失问题,导致传过来的值为null
    写法: int?i=null;

语法T?是Nullable的简写,两者可以相互转换. 可以为null的类型表示其基础值类型正常范围内的值再加上一个null值. 例如,Nullable,其值的范围为-2147483648~2147483647,再加上一个null值.
?经常和??配合使用,比如:

int?i=123;
int j=i??0;
  • 区别readonly和const的使用方法
    使用const的理由只有一个,那就是效率.之所以说const变量的效率高,是因为经过编译器编译后,我们再代码中引用const变量的地方会用const变量所对应的实际值来代替.比如: const=100,const和100被使用的时候是等价,const自带static光圈.
    const和readonly的本质区别如下:
    1.const是编译期常量,readonly是运行期常量
    2.const只能修饰基元类型、枚举类型或字符串类型,readonly没有限制.
    注意:再构造方法内,可以多次对readonly赋值.即在初始化的时候.

  • 将0值作为枚举的默认值
    允许使用的枚举类型有byte、sbyte、short、ushort、int、uint、long和ulong。应该始终将0值作为枚举类型的默认值。不过,这样做不是因为允许使用的枚举类型在声明时的默认值是0值,而是有工程上的意义。
    既然枚举类型从0开始,这样可以避免一个星期多出来一个0值。

  • 避免给枚举类型的元素提供显式的值
    不要给枚举设定值。有时候有某些增加的需要,会为枚举添加元素,在这个时候,就像我们为枚举增加元素ValueTemp一样,极有可能会一不小心增加一个无效值。

  • 习惯重载运算符
    比如:Salary familyIncome=mikeIncome+roseIncome; 阅读一目了然。通过使用opera-tor关键字定义静态成员函数来重载运算符,让开发人员可以像使用内置基元类型一样使用该类型。

  • 创建对象时需要考虑是否实现比较器
    有特殊需要比较的时候就考虑。集合排序比较通过linq 也可以解决。

  • 区别对待==和Equals
    无论是操作符 "==" 还是方法 "Equals" , 都倾向于表达这样一个原则:
    1.对于值类型,如果类型的值相等,就应该返回True.

  1. 对于引用类型,如果类型指向同一个对象,则返回True.
    注意:由于操作符“==”和“Equals”方法从语法实现上来说,都可以被重载为表示“值相等性”和“引用相等性”。所以,为了明确有一种方法肯定比较的是“引用相等性”,FCL中提供了Object.ReferenceEquals方法。该方法比较的是:两个实例是否是同一个实例。
  • 重写Equals时也要重写GetHashCode
    除非考虑到自定义类型会被用作基于散列的集合的键值;否则,不建议重写Equals方法,因为这会带来一系列的问题。
    集合找到值的时候本质上是先去 查找HashCode,然后才查找该对象来比较Equals
    注意:
    重写Equals方法的同时,也应该实现一个类型安全的接口IEquatable<T>,比如 :class Person:IEquatable

  • 为类型输出格式化字符串
    有两种方法可以为类型提供格式化的字符串输出:
    1、一种是意识到类型会产生格式化字符串输出,于是让类型继承接口IFormattable. 这对类型来说,是一种主动实现的方式,要求开发者可以预见类型在格式化方面的要求.
    2、更多的时候,类型的使用者需为类型自定义格式化器,这就是第二种方法,也是最灵活多变的方法,可以根据需求的变化为类型提供多个格式化器

一个典型的格式化器应该继承接口IFormatProvider和ICustomFomatter

  • 正确实现浅拷贝和深拷贝

浅拷贝
将对象中的所有字段复制到新的对象(副本)中。其中,值类型字段的值被复制到副本中后,在副本中的修改不会影响到源对象对应的值。而引用类型的字段被复制到副本中的是引用类型的引用,而不是引用的对象,在副本中对引用类型的字段值做修改会影响到源对象本身。

深拷贝
同样,将对象中的所有字段复制到新的对象中。不过,无论是对象的值类型字段,还是引用类型字段,都会被重新创建并赋值,对于副本的修改,不会影响到源对象本身。

无论是浅拷贝还是深拷贝,微软都建议用类型继承IClone-able接口的方式明确告诉调用者:该类型可以被拷贝。当然,ICloneable接口只提供了一个声明为Clone的方法,我们可以根据需求在Clone方法内实现浅拷贝或深拷贝。
一个简单的浅拷贝的实现代码如下所示:

class Employee:ICloneable
 {    
       public string IDCode {get;set;}   
       public int Age {get;set;  }
       public Department Department{get;set;}    

    #region ICloneable成员 
    public object Clone() 
   {       
    return this.MemberwiseClone(); 
    } 
   #endregion
}

class Department
{    
    public string Name {get;set;}   
    public override string ToString() 
    {      
     return this.Name;  
    }
}

注意到Employee的IDCode属性是string类型。理论上string类型是引用类型,但是由于该引用类型的特殊性(无论是实现还是语义),Object.MemberwiseClone方法仍旧为其创建了副本。也就是说,在浅拷贝过程,我们应该将字符串看成是值类型。

  • 一个简单的深拷贝实现样例如下(建议使用序列化的形式来进行深拷贝)
class Employee:ICloneable
{    
 public string IDCode{get;set;}   
 public int Age{get;set;}    
 public Department Department{get;set;}   

#region ICloneable成员    
public object Clone()    
{        
 using(Stream objectStream=new MemoryStream())        
{            
 IFormatter formatter=new BinaryFormatter();            
 formatter.Serialize(objectStream,this);            
 objectStream.Seek(0,SeekOrigin.Begin);            
 return formatter.Deserialize(objectStream)as Employee;        
}   
}   
 #endregion
}

由于接口ICloneable只有一个模棱两可的Clone方法,所以,如果要在一个类中同时实现深拷贝和浅拷贝,只能由我们自己实现两个额外的方法,声明为DeepClone和Shallow。Em-ployee的最终版本看起来应该像如下的形式:

[Serializable]
class Employee:ICloneable
{   
 public string IDCode{get;set;}    
 public int Age{get;set;}    
 public Department Department{get;set;}   
 #region ICloneable成员    
 public object Clone()   
 {       
  return this.MemberwiseClone();   
  }    

#endregion   
  public Employee DeepClone()    
 {        
     using(Stream objectStream=new MemoryStream())     
    {            
       IFormatter formatter=new BinaryFormatter();
       formatter.Serialize(objectStream,this);            
       objectStream.Seek(0,SeekOrigin.Begin);            
       return formatter.Deserialize(objectStream)as Employee;       
    }   
 }  

  public Employee ShallowClone()   
  {       
     return Clone()as Employee;    
   }
}
  • 利用dynamic来简化反射实现
    dynamic是Framework 4.0的新特性。dynamic的出现让C#具有了弱语言类型的特性。编译器在编译的时候不再对类型进行检查,编译器默认dynamic对象支持开发者想要的任何特性。
    比如,即使你对GetDynamicObject方法返回的对象一无所知,也可以像如下这样进行代码的调用,编译器不会报错:
dynamic dynamicObject=GetDynamicObject();
Console.WriteLine(dynamicObject.Name);
Console.WriteLine(dynamicObject.SampleMethod());

var 与dynamic有巨大的区别
var是编译器的语法糖
dynamic是运行时解析,在编译期时,编译器不对其做任何检查.

  • 反射使用
    不使用dynamic方式
DynamicSample  dynamicSample=new  DynamicSample();
var addMethod=typeof(DynamicSample).GetMethod("Add");
int re=(int)addMethod.Invoke(dynamicSample,new object[] {1,2});

使用dynamic方法

dynamic dynamicSample2=new DynamicSample();
int re2=dynamicSample2.Add(1,2);

//在使用dynamic后,代码看上去更简洁了,并且在可控的范围内减少了一次拆箱的机会。经验证,频繁使用的时候,消耗时间更少

建议: 始终使用dynamic来简化反射实现.

  • 数字指定小数位数/有效位数

指定小数位数

var f = 1.123456;
f.ToString("F3");//"1.123"

指定有效小数位数

var f = 1.123456;
f.ToString("G3");//"1.12"
  • cors跨域头设置
 res.addHeader("Access-Control-Allow-Credentials", "true");
res.addHeader("Access-Control-Allow-Origin", "*");
res.addHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT");
res.addHeader("Access-Control-Allow-Headers", "Content-Type,X-CAF-Authorization-Token,sessionToken,X-TOKEN");

注意: 在http ajax请求中若自己设置了自定义header, 如:token, 则需要在Access-Control-Allow-Headers值中将token添加进去, 如下:res.addHeader("Access-Control-Allow-Headers", "Content-Type,X-CAF-Authorization-Token,sessionToken,X-TOKEN,token");
Access-Control-Allow-Headers中如果没有指定的header则设的跨域就会失败

注意:部分语法适用于 .NET 5 或以上版本。

  • list list = new list(100) ;//在声明时就确定list大小可避免list每次达到快满时自增扩容一倍带来的性能损耗

  • IS用法
    is语义化编程,及结合临时变量做到高效编程


public class Slot : IComparable
{
    public int CompareTo(object obj) { return 0; }

    public int SlotID { get; set; }

    public int ClothesID { get; set; }

    public string ClothesName { get; set; }

    public string SizeName { get; set; }
}

object slot = new Slot() { ClothesName = "上衣" };
{
    if (slot is Slot)
    {
        $"slot is {nameof(Slot)}".Dump();
    }

    var query = (Slot)slot;
    $"slot is {nameof(Slot)},CLothesName={query.ClothesName}".Dump();

    var query2 = slot as Slot;
    if (query2 != null)
    {
        $"slot is {nameof(Slot)},ClothesName={query2.ClothesName}".Dump();
    }

    if (slot is Slot query3)
    {
        $"slot is {nameof(Slot)},ClothesName={query3.ClothesName}".Dump();
    }
}
{
    object e = 150;
    if (e is null) "e is null".Dump();
    if (e is not null) "e is not null".Dump();
    if (e is 150) $"e is {e}".Dump();

    if (e is >= 100 and <= 200) $"e = {e}, 在 >=100 and <=200".Dump();
    if (e is int i and >= 100 and <= 200) $"e = {i}, 在 >=100 and <=200".Dump();
    if (e is 100 or 150 or 200) $"e = {e}, 在 is 100 or 150 or 200".Dump();
    if (e is not null and not "") $"e = {e}, 在 is not null and not ".Dump();
}
(int, int) tp = (1, 2);
if (tp is (1, 2)) "is can check tuple".Dump();
{
    //is 和 var的结合
    int f = 150;
    if (f is var i && i >= 100 && i <= 200) $"f = {i}, 在 >=100 and <=200".Dump();
}

{
  var slotList=new List(){
    new Slot() {SlotID=1, ClothesID=10,ClothesName="上衣",SizeName="L"},
    new Slot() {SlotID=1, ClothesID=20,ClothesName="裤子",SizeName="M"},
    new Slot() {SlotID=1, ClothesID=11,ClothesName="皮带",SizeName="X"},
    new Slot() {SlotID=2, ClothesID=30,ClothesName="上衣",SizeName="L"},
    new Slot() {SlotID=2, ClothesID=40,ClothesName="裤子",SizeName="L"},
  };
  slotList.Select(p=>p.ClothesID).Dump();
   //找到 刚好挂了一件裤子L & 一件上衣L  & 总衣服个数=2  的 挂孔号
   var query = slotList.GroupBy(m=>m.SlotID).Where(m=>m.Where(n=>n.SizeName=="L").ToList()
   is var clothesList && clothesList.Count(k=>k.ClothesName == "裤子") is 1 &&
   clothesList.Count(k=>k.ClothesName == "上衣") is 1 && m.Key==2).ToDictionary(k=>k.Key,v=>v.ToList());
   query.SelectMany(p=>p.Value.Select(s=>s.ClothesID)).Dump();
}

  • 委托:

参考:https://www.cnblogs.com/JerryMouseLi/p/13653940.html

  • IL指令集:

https://www.cnblogs.com/flyingbirds123/archive/2011/01/29/1947626.html

  • 使用 ref struct 做到 0 GC

C# 7 开始引入了一种叫做ref struct的结构,这种结构本质是struct,结构存储在栈内存。但是与struct不同的是,该结构不允许实现任何接口,并由编译器保证该结构永远不会被装箱,因此不会给 GC 带来任何的压力。相对的,使用中就会有不能逃逸出栈的强制限制。

Span就是利用ref struct的产物,成功的封装出了安全且高性能的内存访问操作,且可在大多数情况下代替指针而不损失任何的性能。

ref struct MyStruct
{
    public int Value { get; set; }
}

class RefStructGuid
{
   public static void Test()
    {
        MyStruct x = new MyStruct();
        x.Value = 100;
        Foo(x);

    }

    static void Foo(MyStruct x) { 
        x.Value.Dump();
    }

    static void Bar(object x) { }
}

RefStructGuid.Test();
  • 使用 in 关键字传递不可修改的引用

当参数以ref传递时,虽然传递的是引用但是无法确保引用值不被对方修改,这个时候只需要将ref改为in,便能确保安全性:

void Foo(in string s){
 //s = "22min";
 s.Dump();
}

string s = "wwmin";
Foo(in s);
s.Dump();
  • 在使用大的readonly struct时收益非常明显。

  • 使用 stackalloc 在栈上分配连续内存

对于部分性能敏感却需要使用少量的连续内存的情况,不必使用数组,而可以通过stackalloc直接在栈上分配内存,并使用Span来安全的访问,同样的,这么做可以做到 0 GC 压力。

stackalloc允许任何的值类型结构,但是要注意,Span目前不支持ref struct作为泛型参数,因此在使用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 x = stackalloc int[10];
        for (int i = 0; i < x.Length; i++)
        {
            x[i] = i;
        }
    }
}
  • 使用 Span 操作连续内存

C# 7 开始引入了Span,它封装了一种安全且高性能的内存访问操作方法,可用于在大多数情况下代替指针操作。

static void SpanTest()
{
    Span x = stackalloc int[10];
    for (int i = 0; i < x.Length; i++)
    {
        x[i] = i;
    }

    ReadOnlySpan str = "12345".AsSpan();
    for (int i = 0; i < str.Length; i++)
    {
        Console.WriteLine(str[i]);
    }
}

性能敏感时对于频繁调用的函数使用 SkipLocalsInit

C# 为了确保代码的安全会将所有的局部变量在声明时就进行初始化,无论是否必要。一般情况下这对性能并没有太大影响,但是如果你的函数在操作很多栈上分配的内存,并且该函数还是被频繁调用的,那么这一消耗的副作用将会被放大变成不可忽略的损失。

因此你可以使用SkipLocalsInit这一特性禁用自动初始化局部变量的行为。

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

上述代码将输出不可预期的结果,因为g并没有被初始化为 0。另外,访问未初始化的变量需要在unsafe上下文中使用指针进行访问。

  • 使用模式匹配

有了if-else、as和强制类型转换,为什么要使用模式匹配呢?有三方面原因:性能、鲁棒性和可读性。

为什么说性能也是一个原因呢?因为 C# 编译器会根据你的模式编译出最优的匹配路径。

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

使用模式匹配时,编译器选择了更优的比较方案,你在编写的时候无需考虑如何组织判断语句,心智负担降低,并且可读性和简洁程度显然更好,有哪些条件分支一目了然。

编译器非常智能地为你选择了最佳的方案。
代码非常简洁,而且数据的流向一眼就能看清楚,就算是没有接触过这部分代码的人看一下模式匹配的过程,也能一眼就立刻掌握各分支的情况,而不需要在一堆的if-else当中梳理这段代码到底干了什么。

  • 使用局部函数而不是 lambda 创建临时委托

在使用Expression>作为参数的 API 时,使用 lambda 表达式是非常正确的,因为编译器会把我们写的 lambda 表达式编译成 Expression Tree,而非直观上的函数委托。

而在单纯只是Func<>、Action<>时,使用 lambda 表达式恐怕不是一个好的决定,因为这样做必定会引入一个新的闭包,造成额外的开销和 GC 压力。从 C# 8 开始,我们可以使用局部函数很好的替换掉 lambda:

int SomeMethod(Func 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时,大多数情况下只是需要简单的对其进行await而已,而并不需要将其保存下来以后再await,那么Task提供的很多的功能则并没有被使用,反而在高并发下,由于反复分配Task导致 GC 压力增加。

这种情况下,我们可以使用ValueTask代替Task

ValueTask Foo()
{
    return ValueTask.FromResult(1);
}

async ValueTask Caller()
{
    await Foo();
}

由于ValueTask是值类型结构,因此该对象本身不会在堆上分配内存,于是可以减轻 GC 压力。

  • 实现解构函数代替创建元组

如果我们想要把一个类型中的数据提取出来,我们可以选择返回一个元组,其中包含我们需要的数据:

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的开销,如果我们将代码改成实现解构方法:

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()的调用,同时还没有任何的额外开销。你可以看到实现 Deconstruct 函数并不需要让你的类型实现任何的接口,从根本上杜绝了装箱的可能性,这是一种 0 开销抽象。另外,解构函数还能用于做模式匹配,你可以像使用元组一样地使用解构函数(下面代码的意思是,当x为 3 时取y,否则取x + y):

static void Bar2(Foo2 v)
{
    {
        var (x, y) = v;
        Console.WriteLine($"X = {x}, Y = {y}");
    }
   
    var result = v switch
    {
        Foo2(3, var y) => y,
        Foo2(var x, var y) => x + y,
        _ => 0
    };
}

注: 本人c#代码中部分使用了LinqPad工具, Dump为该工具打印输出的语法,类似Console.WriteLine, 特此说明.

  • 使用sealed密封类
    sealed 修饰符可阻止其他类继承自该类 . 使用密封类能提高性能
    abstract 修饰符与密封类结合使用是错误的,因为抽象类必须由提供抽象方法或属性的实现的类来继承。
    应用于方法或属性时,sealed 修饰符必须始终与 override 结合使用。

版权声明:
作者:倾城
链接:https://www.techfm.club/p/43260.html
来源:TechFM
文章版权归作者所有,未经允许请勿转载。

THE END
分享
二维码
< <上一篇
下一篇>>