c#5.0/6.0/7.0

发现很多.net 程序员水平一直停留在c#3.0阶段,现在来整理下c#5.0/6.0/7.0新语法新特性。

人生需要不断充电,不断去get新技能而不是固步自封,对于我自己而言,虽不盲目追求新技术,但每当有新技术出现时也会去了解一下,因为我一直认为,存在 就有它的价值所在。

C# 3.5

扩展方法

扩展方法所在的类和扩展方法必须是静态的 并且扩展方法第一个参数是要扩展的类名 并在this,

拓展方法这个真的很实用,我在我的所有项目中都封装了一套值转换的拓展方法,比如

     public static string ConvertToString(this object s)
     {
            if (s == null)
            {
                return "";
            }
            else
            {
                return Convert.ToString(s);
            }
     }
     public static Int32 ConvertToInt32(this string s)
     {
            int i = 0;
            if (s == null)
            {
                return 0;
            }
            else
            {
                int.TryParse(s, out i);
            }
            return i;
     }

 

 

比如我封装了2个拓展方法ConvertToString和ConvertToInt32,在我需要把字符串转int的时候,传统的做法可能是 int.Parse("123")  又或者是Convert.ToInt32("123") 但是无论是哪种方法,都会引起异常,而当我封装了拓展方法以后 我可以直接用 "123".ConvertToInt32()  则可以进行转换,如果是"a".ConvertToInt32() 则会返回0,而不是向原有一样去做try...catch

 

C# 5.0

异步文件 I/O

在.Net 4.5中,通过async和await两个关键字,引入了一种新的基于任务的异步编程模型(TAP)。在这种方式下,可以通过类似同步方式编写异步代码,极大简化了异步编程模型。如下式一个简单的实例:  
    static async void DownloadStringAsync2(Uri uri)
    {
        var webClient = new WebClient();
        var result = await webClient.DownloadStringTaskAsync(uri);
        Console.WriteLine(result);
    }

 

 

而之前的方式是这样的

    static void DownloadStringAsync(Uri uri)
    {
        var webClient = new WebClient();
        webClient.DownloadStringCompleted += (s, e) =>
            {
                Console.WriteLine(e.Result);
            };
        webClient.DownloadStringAsync(uri);
    }

 

也许前面这个例子不足以体现async和await带来的优越性,下面这个例子就明显多了:

    public void CopyToAsyncTheHardWay(Stream source, Stream destination)
    {
        byte[] buffer = new byte[0x1000];
        Action<IAsyncResult> readWriteLoop = null;
        readWriteLoop = iar =>
        {
            for (bool isRead = (iar == null); ; isRead = !isRead)
            {
                switch (isRead)
                {
                    case true:
                        iar = source.BeginRead(buffer, 0, buffer.Length,
                            readResult =>
                            {
                                if (readResult.CompletedSynchronously) return;
                                readWriteLoop(readResult);
                            }, null);
                        if (!iar.CompletedSynchronously) return;
                        break;
                    case false:
                        int numRead = source.EndRead(iar);
                        if (numRead == 0)
                        {
                            return;
                        }
                        iar = destination.BeginWrite(buffer, 0, numRead,
                            writeResult =>
                            {
                                if (writeResult.CompletedSynchronously) return;
                                destination.EndWrite(writeResult);
                                readWriteLoop(null);
                            }, null);
                        if (!iar.CompletedSynchronously) return;
                        destination.EndWrite(iar);
                        break;
                }
            }
        };
        readWriteLoop(null);
    }

    public async Task CopyToAsync(Stream source, Stream destination)
    {
        byte[] buffer = new byte[0x1000];
        int numRead;
        while ((numRead = await source.ReadAsync(buffer, 0, buffer.Length)) != 0)
        {
            await destination.WriteAsync(buffer, 0, numRead);
        }
    }

 

 

在lambdas中使用循环变量

技术上,这个对长期存在的困扰和煎熬的修正,但是使得C#增加了可用性,所以我将会提及它。

自从C# 3以来,编写匿名函数比命名的更见快捷和容易了,匿名函数被广泛地使用于LINQ,但是他们也在其他情况下被使用,如你想要在不需要授权的巨大层级类和接口以及可见函数中快速的拥有参数化行为。匿名函数的一个重要特性就是你可以从本地环境中捕获变量,以下是一个示例:

public static IEnumerable<int> GetGreaterThan(IEnumerable<int> source, int n) {
  return source.Where(i => i > n);
}

 

 

看这里,i=>i>n是一个捕获了n值的匿名函数。例如如果n是17,那么该函数便是 i=>i>17

 在C#之前的版本中,如果你编写了一个循环,你不能在lambda中使用循环变量。事实上,它比想象中更糟。你可以在lambda中使用循环变量,但是它将给你一个错误的结果。它会使用循环退出时的玄幻变量值,不是呗捕获时的值。

例如,下面是一个返回“加法器”集合的函数:

public static List<Func<int, int>> GetAdders(params int[] addends) {
  var funcs = new List<Func<int, int>>();
  foreach (int addend in addends) {
    funcs.Add(i => i + addend);
  }
  return funcs;
}

 

C# 6.0

1. 自动的属性初始化器Auto Property initialzier

之前的方式

初始化一个自动属性Auto Property的唯一方式,就是去实现一个明确的构造器,在里面对属性值进行设置.

public class AutoPropertyBeforeCsharp6
{
    private string _postTitle = string.Empty;
    public AutoPropertyBeforeCsharp6()
    {
        //assign initial values
        PostID = 1;
        PostName = "Post 1";
    }
 
    public long PostID { get; set; }
 
    public string PostName { get; set; }
 
    public string PostTitle
    {
        get { return _postTitle; }
        protected set
        {
            _postTitle = value;
        }
    }
}

 

 

有了这个特性之后的方式

使用 C# 6 自动实现的带有初始值的属性可以不用编写构造器就能被初始化. 我们可以用下面的代码简化上面的示例:

public class AutoPropertyInCsharp6
{
    public long PostID { get;  } = 1;
 
    public string PostName { get; } = "Post 1";
 
    public string PostTitle { get; protected set; } = string.Empty;
}

 

2. 主构造器

我们使用构造器主要是来初始化里面的值.(接受参数值并将这些参数值赋值给实体属性).

之前的方式

 

public class PrimaryConstructorsBeforeCSharp6
{
    public PrimaryConstructorsBeforeCSharp6(long postId, string postName, string postTitle)
    {
        PostID = postId;
        PostName = postName;
        PostTitle = postTitle; 
    }
 
    public long PostID { get; set; }
    public string PostName { get; set; }
    public string PostTitle { get; set; }
}

 

有了这个特性之后的方式

 

public class PrimaryConstructorsInCSharp6(long postId, string postName, string postTitle)
{        
    public long PostID { get;  } = postId;
    public string PostName { get; } = postName;
    public string PostTitle { get;  } = postTitle;
}

 

 

3. 字典初始化器

之前的方式

编写一个字典初始化器的老办法如下

public class DictionaryInitializerBeforeCSharp6
{
    public Dictionary<string, string> _users = new Dictionary<string, string>()
    {
        {"users", "Venkat Baggu Blog" },
        {"Features", "Whats new in C# 6" }
    };
}

 

 

有了这个特性之后的方式

我们可以像数组里使用方括号的方式那样定义一个字典初始化器

public class DictionaryInitializerInCSharp6
{
    public Dictionary<string, string> _users { get; } = new Dictionary<string, string>()
    {
        ["users"]  = "Venkat Baggu Blog",
        ["Features"] =  "Whats new in C# 6" 
    };
}

 

4. 声明表达式

之前的方式

 

public class DeclarationExpressionsBeforeCShapr6()
{
    public static int CheckUserExist(string userId)
    {
        //Example 1
        int id;
        if (!int.TryParse(userId, out id))
        {
            return id;
        }
        return id;
    }
 
    public static string GetUserRole(long userId)
    {
        ////Example 2
        var user = _userRepository.Users.FindById(x => x.UserID == userId);
        if (user!=null)
        {
            // work with address ...
 
            return user.City;
        }
    }
}

 

有了这个特性之后的方式

在 C# 6 中你可以在表达式的中间声明一个本地变量. 使用声明表达式我们还可以在if表达式和各种循环表达式中声明变量

public class DeclarationExpressionsInCShapr6()
{
    public static int CheckUserExist(string userId)
    {
        if (!int.TryParse(userId, out var id))
        {
            return id;
        }
        return 0;
    }
 
    public static string GetUserRole(long userId)
    {
        ////Example 2
        if ((var user = _userRepository.Users.FindById(x => x.UserID == userId) != null)
        {
            // work with address ...
 
            return user.City;
        }
    }
}

 

 

5. 静态的 Using

之前的方式

对于你的静态成员而言,没必要为了调用一个方法而去弄一个对象实例. 你会使用下面的语法

TypeName.MethodName
public class StaticUsingBeforeCSharp6
{
    public void TestMethod()
    {
        Console.WriteLine("Static Using Before C# 6");
    }
}

 

之后的方式

在 C# 6 中,你不用类名就能使用 静态成员 . 你可以在命名空间中引入静态类.

如果你看了下面这个实例,就会看到我们将静态的Console类移动到了命名空间中

using System.Console;
namespace newfeatureincsharp6
{
    public class StaticUsingInCSharp6
    {
        public void TestMethod()
        {
            WriteLine("Static Using Before C# 6");
        }
    }
}

 

看到这里各位小伙伴有木有惊呆了!!没错,你可以引用静态类了。

6. catch块里面的await

C# 6 之前catch和finally块中是不能用 await 关键词的. 在 C# 6 中,我们终于可以再这两个地方使用await了.

try 
{          
  //Do something
}
catch (Exception)
{
  await Logger.Error("exception logging")
}

 

如果不知道await是干嘛的看上面的异步I/O

7. 异常过滤器

异常过滤器可以让你在catch块执行之前先进行一个 if 条件判断.

看看这个发生了一个异常的示例,现在我们想要先判断里面的Exception是否为null,然后再执行catch块

//示例 1
try
{
    //Some code
}
catch (Exception ex) if (ex.InnerException == null)
{
    //Do work
 
}
 
//Before C# 6 we write the above code as follows
 
//示例 1
try
{
    //Some code
}
catch (Exception ex) 
{
    if(ex.InnerException != null)
    {
        //Do work;
    }
}

 

8. 用于检查NULL值的条件访问操作符?.

看看这个实例,我们基于UserID是否不为null这个条件判断来提取一个UserRanking.

之前的方式

var userRank = "No Rank";
if(UserID != null)
{
    userRank = Rank;
}
 
//or
 
var userRank = UserID != null ? Rank : "No Rank"

 

有了这个特性之后方式

var userRank = UserID?.Rank ?? "No Rank";

 

 

 

C# 7.0

输出变量

在当前的 C# 中,使用输出参数并不像我们想的那样方便。在你调用一个无输出参数的方法之前,首先必须声明一个变量并传递给它。如果你没有初始化这些变量,你就无法使用 var 来声明它们,除非先指定完整的类型:

public void PrintCoordinates(Point p)
{
    int x, y; // have to "predeclare"
    p.GetCoordinates(out x, out y);
    WriteLine($"({x}, {y})");
}

 

 

在 C#7.0 中,我们正在增加输出变量和声明一个作为能够被传递的输出实参的变量的能力:

public void PrintCoordinates(Point p)
{
    p.GetCoordinates(out int x, out int y);
    WriteLine($"({x}, {y})");
}

 

 

注意,变量是在封闭块的范围内,所以后续也可以使用它们。大多数类型的声明不建立自己的范围,因此在他们中声明的变量通常会被引入到封闭范围。

Note:在 Preview 4 中,适用范围规则更为严格:输出变量的作用域是声明它们的语句,因此直到下个版本发布时,上面的示例才会起作用。

由于输出变量直接被声明为实参传递给输出形参,编译器通常会告诉他们应该是的类型(除非有冲突过载),所以使用 var 来代替声明它们的方式是比较好的:

p.GetCoordinates(out var x, out var y);

 

模式匹配

C# 7.0 引入了模式概念。抽象地讲,模式是句法元素,能用来测试一个数据是否具有某种“形”,并在被应用时,从值中提取有效信息。

C#7.0 中的模式示例:

  • C 形式的常量模式(C是C#中的常量表达式),可以测试输入是否等于C
  • T X 形式的类型模式(T是一种类型、X是一个标识符),可以测试输入是否是T类型,如果是,会将输入值提取成T类型的新变量X
  • Var x 形式的 Var 模式(x是一个标识符),它总是匹配的,并简单地将输入值以它原本的类型存入一个新变量X中。

这仅仅是个开始 - 模式是一种新型的 C# 中的语言元素。未来,我们希望增加更多的模式到 C# 中。

在 C#7.0,我们正在加强两个现有的具有模式的语言结构:

  • is 表达式现在具有一种右手侧的模式,而不仅仅是一种类型
  • switch 语句中的 case 语句现在可以使用匹配模式,不只是常数值

在 C#的未来版本中,我们可能会增加更多的被用到的模式。

具有模式的 IS 表达式

下面是使用 is 表达式的示例,其中利用了常量模式和类型模式:

public void PrintStars(object o)
{
    if (o is null) return;     // constant pattern "null"
    if (!(o is int i)) return; // type pattern "int i"
    WriteLine(new string('*', i));
}

 

 

正如你们看到,模式变量(模式引入的变量)和早前描述的输出变量比较类似,它们可以在表达式中间声明,并在最近的范围内使用。就像输出变量一样,模式变量是可变的。

注:就像输出变量一样,严格范围规则适用于Preview 4。

模式和 Try方法可以很好地协同:

if (o is int i || (o is string s && int.TryParse(s, out i)) { /* use i */ }

 

具有模式的 Switch 语句

我们正在归纳 Switch 语句:

  • 可以设定任何类型的 Switch 语句(不只是原始类型)
  • 模式可以用在 case 语句中
  • Case 语句可以有特殊的条件

下面是一个简单的例子:

switch(shape)
{
    case Circle c:
        WriteLine($"circle with radius {c.Radius}");
        break;
    case Rectangle s when (s.Length == s.Height):
        WriteLine($"{s.Length} x {s.Height} square");
        break;
    case Rectangle r:
        WriteLine($"{r.Length} x {r.Height} rectangle");
        break;
    default:
        WriteLine("<unknown shape>");
        break;
    case null:
        throw new ArgumentNullException(nameof(shape));
}

 

c#7.0 switch支持不同类型的一起switch了。

关于新扩展的 switch 语句,有几点需要注意:

  • Case 语句的顺序现在变得重要:就像 catch 语句一样,case 语句的范围现在可以相交,第一个匹配上的会被选中。此外,就像 catch 语句一样,编译器通过去除明显不会进入的 case 来帮助你。在此之前,你甚至不需要告诉判断的顺序,所以这并不是一个使用 case 语句的巨大的改变。
  • 默认的语句还是最后被判断:尽管 null 的 case 语句在最后语句之前出现,它也会在默认语句被选中之前被测试。这是与现有 Switch 语义兼容的。然而,好的做法通常会将默认语句放到最后。
  • Switch 不会到最后的 null 语句:这是因为当前 IS 表达式的例子具有类型匹配,不会匹配到 null。这保证了空值不会不小心被任何的类型模式匹配上的情况;你必须更明确如何处理它们(或放弃它而使用默认语句)。

通过一个 case 引入模式变量:标签仅在相应的 Switch 范围内。

元组

这是一个从方法中返回多个值的常见模式。目前可选用的选项并非是最佳的:

  • 输出参数:使用起来比较笨拙(即使有上述的改进),他们在使用异步方法是不起作用的。
  • System.Tuple<...> 返回类型:冗余使用和请求一个元组对象的分配。
  • 方法的定制传输类型:对于类型,具有大量的代码开销,其目的只是暂时将一些值组合起来。
  • 通过动态返回类型返回匿名类型:很高的性能开销,没有静态类型检查。

在这点要做到更好,C#7.0 增加的元组类型和元组文字:

(string, string, string) LookupName(long id) // tuple return type
{
    ... // retrieve first, middle and last from data storage
    return (first, middle, last); // tuple literal
}

 

这个方法可以有效地返回三个字符串,以元素的形式包含在一个元组值里。

这种方法的调用将会收到一个元组,并且可以单独地访问其中的元素:

var names = LookupName(id);
WriteLine($"found {names.Item1} {names.Item3}.");

 

Item1 等是元组元素的默认名称,也可以被一直使用。但他们不具有描述性,所以你可以选择添加更好的:

(string first, string middle, string last) LookupName(long id) // tuple elements have names

 

现在元组的接收者有多个具有描述性的名称可用:

var names = LookupName(id);
WriteLine($"found {names.first} {names.last}.");

 

你也可以直接在元组文字指定元素名称:

return (first: first, middle: middle, last: last); // named tuple elements in a literal

 

一般可以给元组类型分配一些彼此无关的名称:只要各个元素是可分配的,元组类型就可以自如地转换为其他的元组类型。也有一些限制,特别是对元组文字,即常见的和告警错误,如不慎交换元素名称的情况下,就会出现错误。

Note:这些限制尚未在 Preview 4 中实现。

元组是值类型的,它们的元素是公开的,可变的。他们有值相等,如果所有的元素都是成对相等的(并且具有相同的哈希值),那么这两个元组也是相等的(并且具有相同的哈希值)。

这使得在需要返回多个值的情况下,元组会非常有用。举例来说,如果你需要多个 key 值的字典,使用元组作为你的 key 值,一切会非常顺利。如果你需要在每个位置都具有多个值的列表,使用元组进行列表搜索,会工作的很好。

解构

消耗元组的另一种方法是将解构它们。一个解构声明是一个将元组(或其他值)分割成部分并单独分配到新变量的语法:

(string first, string middle, string last) = LookupName(id1); // deconstructing declaration
WriteLine($"found {first} {last}.");

 

在解构声明中,您可以使用 var 来声明单独的变量:

(var first, var middle, var last) = LookupName(id1); // var inside

 

或者将一个单独的 var 作为一个缩写放入圆括号外面:

var (first, middle, last) = LookupName(id1); // var outside

 

你也可以使用解构任务来解构成现有的变量

(first, middle, last) = LookupName(id2); // deconstructing assignment

 

解构不只是应用于元组。任何的类型都可以被解构,只要它具有(实例或扩展)的解构方法:

public void Deconstruct(out T1 x1, ..., out Tn xn) { ... }

 

输出参数构成了解构结果中的值。

(为什么它使用了参数,而不是返回一个元组?这是为了让你针对不同的值拥有多个重载)。

class Point
{
    public int X { get; }
    public int Y { get; }
 
    public Point(int x, int y) { X = x; Y = y; }
    public void Deconstruct(out int x, out int y) { x = X; y = Y; }
}
(var myX, var myY) = GetPoint(); // calls Deconstruct(out myX, out myY);

 

这是一种常见的模式,以一种对称的方式包含了构建和解构。

对于输出变量,我们计划在解构中加入通配符,来化简你不关心的变量:

(var myX, *) = GetPoint(); // I only care about myX

 

局部函数

有时候,一个辅助函数可以在一个独立函数内部起作用。现在,你可以以一个局部函数的方式在其它函数内部声明这样的函数:

public int Fibonacci(int x)
{
    if (x < 0) throw new ArgumentException("Less negativity please!", nameof(x));
    return Fib(x).current;
 
    (int current, int previous) Fib(int i)
    {
        if (i == 0) return (1, 0);
        var (p, pp) = Fib(i - 1);
        return (p + pp, p);
    }
}

 

内的参数和局部变量在局部函数的内部是可用的,就如同它们在 lambda 表达式中一样。

举一个例子,迭代的方法实现通常需要一个非迭代的封装方法,以便在调用时检查实参。(迭代器本身不启动运行,直到 MoveNext 被调用)。局部函数非常适合这样的场景:

public IEnumerable<T> Filter<T>(IEnumerable<T> source, Func<T, bool> filter)
{
    if (source == null) throw new ArgumentNullException(nameof(source));
    if (filter == null) throw new ArgumentNullException(nameof(filter));
 
    return Iterator();
 
    IEnumerable<T> Iterator()
    {
        foreach (var element in source) 
        {
            if (filter(element)) { yield return element; }
        }
    }
}

 

文字改进

C#7.0 允许 _ 出现,作为数字分隔号:

var d = 123_456;
var x = 0xAB_CD_EF;

 

你可以将 _ 放入任意的数字之间,以提高可读性,它们对值没有影响。

此外,C#7.0 引入了二进制文字,这样你就可以指定二进制模式而不用去了解十六进制。

var b = 0b1010_1011_1100_1101_1110_1111;

 



引用返回和局部引用

就像在 C# 中通过引用来传递参数(使用引用修改器),你现在也可以通过引用来返回参数,同样也可以以局部变量的方式存储参数。

public ref int Find(int number, int[] numbers)
{
    for (int i = 0; i < numbers.Length; i++)
    {
        if (numbers[i] == number) 
        {
            return ref numbers[i]; // return the storage location, not the value
        }
    }
    throw new IndexOutOfRangeException($"{nameof(number)} not found");
}
 
int[] array = { 1, 15, -39, 0, 7, 14, -12 };
ref int place = ref Find(7, array); // aliases 7's place in the array
place = 9; // replaces 7 with 9 in the array
WriteLine(array[4]); // prints 9

 

这是绕过占位符进入大数据结构的好方法。例如,一个游戏也许会将它的数据保存在大型预分配的阵列结构中(为了避免垃圾回收机制暂停)。方法可以将直接引用返回成一个结构,通过它的调用者可以读取和修改它。

也有一些限制,以确保安全:

  • 你只能返回“安全返回”的引用:一个是传递给你的引用,一个是指向对象中的引用。
  • 本地引用会被初始化成一个本地存储,并且不能指向另一个存储。

异步返回类型

到现在为止,C# 的异步方法必须返回 void,Task 或 Task<T>。C#7.0 允许其它类型以这种能从一个方法中返回的方式被定义,因为它们可以以异步方法被返回的方式来定义其它类型。

例如我们计划建立一个 ValueTask<T> 构类型的数据。建立它是为了防止异步运行的结果在等待时已可用的情境下,对 Task<T> 进行分配。对于许多实例中设计缓冲的异步场景,这可以大大减少分配的数量并显著地提升性能。

Note:异步返回类型尚未在 Preview 4 中提供。

更多的 expression bodied 成员:

expression bodied 的方法和属性是对 C# 6.0 的巨大提升。C# 7.0 为 expression bodied 事件列表增加了访问器,结构器和终结器。

class Person
{
    private static ConcurrentDictionary<int, string> names = new ConcurrentDictionary<int, string>();
    private int id = GetId();
 
    public Person(string name) => names.TryAdd(id, name); // constructors
    ~Person() => names.TryRemove(id, out *);              // destructors
    public string Name
    {
        get => names[id];                                 // getters
        set => names[id] = value;                         // setters
    }
}

 

Throw 表达式 

在表达式中间抛出一个异常是很容易的:只需为自己的代码调用一个方法!但在 C#7.0 中,我们允许在任意地方抛出一个表达式:

class Person
{
    public string Name { get; }
    public Person(string name) => Name = name ?? throw new ArgumentNullException(name);
    public string GetFirstName()
    {
        var parts = Name.Split(" ");
        return (parts.Length > 0) ? parts[0] : throw new InvalidOperationException("No name!");
    }
    public string GetLastName() => throw new NotImplementedException();
}

 

 

好了,新特性比较实用的部分整理到这里。希望大家还是要跟上时代的步伐,不断进步,不断去学习!!!!

posted @ 2018-07-16 16:46  许泽宇  阅读(3721)  评论(1编辑  收藏  举报