C# 7.0 可以在 VS 17 使用,这个版本我下载企业版上传百度云,有需要可以到文章最后去[下载](#VS 17 下载)。
本文主要:C# 7.0 带来的新功能
out 返回值定义
Tuples
模式匹配
ref 返回本地值
内部函数
全部地方可以支持辣么大
在表达式扔异常
广义异步返回类型
数值常量语法
顺便告诉大家 7.1 的新特性
支持 async 的主函数
默认值不需要写类型
自动推断 Tuple 的名称
值引用
指定位置命名参数
private protected 访问修饰
字符常量下划线可以放在最前
C# 7.0的功能主要是数据处理,让代码更简洁,让代码性能更高
让代码简单这个我觉得不如6.0,性能WR为了Iot做的。C#速度差,垃圾wr就让C#可以直接访问内存,让速度变快,这个下面没有说
C# 7.0 最好的是 使用 Tuple 。虽然之前也有,但是现在版本比较好用。实际抄袭了某脚本。
修改大的有 Case 。模式匹配,可以判断类型,其实这个使用是我们有类 a,类b、c继承a,这时使用就比较好,如何使用在下面会说。
如果觉得这个功能没有用,可以去 Visual studio 按反馈喷
如果好奇他是怎么弄,可以查看https://github.com/dotnet/roslyn
out 返回值定义
我们以前要使用 out 总是需要在外面定义我们变量。
首先定义一个 变量,使用函数,这样觉得需要多写代码
public void PrintCoordinates ( Point p )
p. GetCoordinates ( out x, out y);
WriteLine ( $" ( { x } , { y } ) " );
在7.0我们可以使用在 out 定义我们变量,这样看起来不是在一个区域,但是可以减少我的代码
public void PrintCoordinates ( Point p )
p. GetCoordinates ( out int x , out int y );
WriteLine ( $" ( { x } , { y } ) " );
在 out 定义类型,定义可以用var
看到这我才说这样有用,如果我们开始没有确定我们返回的是什么,然后直接定义,需要修改地方多,但是如果我们使用Var就可以让我们定义修改少,一般在写就需要先想我们需要用什么,不要总是改
如果我们使用一个返回为bool,那么可以在{使用out的值
public void PrintStars ( string s )
if ( int . TryParse (s, out var i )) { WriteLine ( new string ( '*' , i)); }
else { WriteLine ( " Cloudy - no stars tonight! " ); }
== 下面代码被WR删了,以前有说到,现实wr没有做
如果有返回值我们不需要,可以out *,这样我们就不用知道这个返回值,原先不需要使用我还要想一个变量,然后vs说我这个没有使用,现在我们直接就不给他名
在我们下面有返回多个,这时不需要的可以用*
public void PrintStars ( string s )
if ( int . TryParse (s, out * )) { WriteLine ( " 转换成功 " ); }
else { WriteLine ( " 转换失败 " ); }
== 上面代码WR没有做,不需要的返回值是可以使用_
模式匹配
模式匹配是包括 is 和 switch ,下面先说 is
C# 7.0可以使用 is 一部分代替 as
我们需要判断一个值是否是一个类型,如果是那么赋值,在以前,使用的代码需要两行
还可以使用 as
但是在新的C#,可以使用
o is int i
那么我们就可以直接使用i
在我们一个判断,如果我们存在了object o是int,那么我们就使用int i=(int)o;
如果我们object不是int,那么转换object o是string,string s=(string)o;
这也就是对下面的语句
int.TryParse(s,out i);
我们可以简化,判断是不是int,如果是给i,这时就返回true
然后判断是不是string,是就转,成功使用i
if (o is int i || (o is string s && int.TryParse(s, out i)) { /* use i */ }
但是 is 的用法在于 switch
我们在Case可以选择类型
WriteLine ( $" circle with radius { c . Radius } " );
case Rectangle s when (s.Length == s.Height):
WriteLine ( $" { s . Length } x { s . Height } square " );
WriteLine ( $" { r . Length } x { r . Height } rectangle " );
WriteLine ( " <unknown shape> " );
throw new ArgumentNullException ( nameof (shape));
case 顺序很重要,可以看到可以判断类型,但是 case 还可以混合判断。
throw new InvalidOperationException ( " unknown item type " );
case var @var when (@var != null && ( int ) (@var) == 45 ):
// The order of case clauses now matters!
case IEnumerable < object > subList when subList. Any ():
case IEnumerable < object > subList :
注意 default 在最后,即使他后面有语句,除非存在语句识别,那么最后会执行他。
Tuples
以前我们需要返回多个有点难,可以使用out参数,可以Tuples<string,double>
我们做了修改,可以使用新的方法,这样我们返回多个就可以直接和某垃圾语言那返回
( string , string , string ) LookupName ( long id) // tuple return type
.. . // 返回多个数据,我们在数据拿到多个数据
return (first, middle, last); // tuple literal
var names = LookupName(id);
我们这样用第一返回值:names.Item1
和原来几乎没有修改,这样对于返回值不好,因为我们难以去记,哪个返回值是什么
我们要给他一个好记的 变量,可以写在函数定义
(string first, string middle, string last) LookupName(long id)
我们使用第一个names.first
,这样使用就容易,原因是可以给一个表达他是意思的变量。
返回可以使用return (first, middle, last);
,必须和之前定义顺序一样,但如果定义了名称,可以使用
return last:last,first:first
这个方法是很好的,不需要和定义的顺序那样。
对于调用函数,可以使用一个变量,可以使用多个变量
( string first , string middle , string last ) = LookupName (id1);
var name = LookupName (id1);
可以看到两个代码,作用一样,但是第一个代码除了使用变量类型,同样可以使用 var
( var fist , var midd ) = Lookup (id);
如果我们有多个var,那么我们可以简单var (first, middle, last) = LookupName(id1);
定义所有变量
除了方法使用,可以在变量使用
var sumNew = ( first : 1 , count : 20 );
这样就定义了一个,可以使用他的名称,不使用 item
原先的,也就是在定义,给他变量。
上面代码的意思:可以定义一个包括每项名称的变量,可以在使用时,用定义的变量
var sumNew = ( first : 1 , count : 20 );
Console. WriteLine ( $" first { sumNew . first } count { sumNew . count } " );
如果不想在定义写变量,那么可以修改var
,作为变量
( int first , int count ) sum = ( 1 , 20 );
Console. WriteLine ( $" first { sum . first } count { sum . count } " );
这里,类型int
不能写 var
如果想不到变量,那么只能使用
( int , int ) sum = ( 1 , 20 );
Console. WriteLine ( $" first { sum . Item1 } count { sum . Item2 } " );
如果使用的不是 .net Framework 4.7 那么引用 ValueTuple 才可以。除了支持多个返回值,实际 ValueTuple 可以作为附加在类的新字段,一般在列表使用。
例如我有一个类 Foo 里面只有一个属性 Name ,但是在 ViewModel 需要他有 Check 属性表示是否选中。那么可以使用 ValueTuple 添加这个属性。
List < (Foo foo, bool check) > Foo
本地函数
我们可以在函数里面定义函数,这是本地函数
public int Fibonacci ( int x )
if (x < 0 ) throw new ArgumentException ( " Less negativity please! " , nameof (x));
( int current, int previous) Fib ( int i)
if (i == 0 ) return ( 1 , 0 );
var ( p , pp ) = Fib (i - 1 );
以前有些函数只会使用一次,但是他的功能多,所以就把它写成方法,于是一个类就很多这种方法,只会在一个函数使用,但是写成方法,有时候开始看他,会觉得方法很多,不知道哪个方法在哪使用。
上面说的是那些没使用 vs 企业版的兄弟,其实有了企业版,没有这问题。
现在可以使用内部函数,在一个函数里定义函数,看上面的代码,写一个斐波纳算法,可以直接使用函数里函数,不需要定义方法。
这个用法在:迭代,异步
对于迭代器,抛出异常在使用,不是创建。
看下这代码
public static IEnumerable < char > AlphabetSubset ( char start , char end )
if ((start < 'a' ) || (start > 'z' ))
throw new ArgumentOutOfRangeException ( paramName : nameof (start), message : " start must be a letter " );
if ((end < 'a' ) || (end > 'z' ))
throw new ArgumentOutOfRangeException ( paramName : nameof (end), message : " end must be a letter " );
throw new ArgumentException ( $" { nameof ( end ) } must be greater than { nameof ( start ) } " );
for ( var c = start; c < end; c ++ )
在输入不合法,就会抛出异常,那么抛出异常的时候是什么
var resultSet = Iterator. AlphabetSubset ( 'f' , 'a' );
Console. WriteLine ( " iterator created " );
foreach ( var thing in resultSet)
Console. Write ( $" { thing } , " );
可以看到在 var resultSet = Iterator.AlphabetSubset('f', 'a');
不会抛出异常,在 Console.Write($"{thing}, ");
抛出异常。
很难定位到是在哪的异常,出现异常和知道异常的,不在一个地方,这就是之前使用迭代的一个比较难发现的。
所以做法是新建一个方法迭代
public static IEnumerable < char > AlphabetSubset2 ( char start , char end )
if ((start < 'a' ) || (start > 'z' ))
throw new ArgumentOutOfRangeException ( paramName : nameof (start), message : " start must be a letter " );
if ((end < 'a' ) || (end > 'z' ))
throw new ArgumentOutOfRangeException ( paramName : nameof (end), message : " end must be a letter " );
throw new ArgumentException ( $" { nameof ( end ) } must be greater than { nameof ( start ) } " );
return alphabetSubsetImplementation (start, end);
private static IEnumerable < char > alphabetSubsetImplementation ( char start , char end )
for ( var c = start; c < end; c ++ )
这样就可以定位,但是问题是,可能错误调用 alphabetSubsetImplementation ,直接使用 他,不是使用 AlphabetSubset2 ,所以在新的C#,可以使用内部方法
public static IEnumerable < char > AlphabetSubset3 ( char start , char end )
if ((start < 'a' ) || (start > 'z' ))
throw new ArgumentOutOfRangeException ( paramName : nameof (start), message : " start must be a letter " );
if ((end < 'a' ) || (end > 'z' ))
throw new ArgumentOutOfRangeException ( paramName : nameof (end), message : " end must be a letter " );
throw new ArgumentException ( $" { nameof ( end ) } must be greater than { nameof ( start ) } " );
return alphabetSubsetImplementation ();
IEnumerable < char > alphabetSubsetImplementation ()
for ( var c = start; c < end; c ++ )
同时,在异步,如果出现异常,也是难以定位,所以可以用内部方法在异步前判断异常
public Task < string > PerformLongRunningWork ( string address , int index , string name )
if ( string . IsNullOrWhiteSpace (address))
throw new ArgumentException ( message : " An address is required " , paramName : nameof (address));
throw new ArgumentOutOfRangeException ( paramName : nameof (index), message : " The index must be non-negative " );
if ( string . IsNullOrWhiteSpace (name))
throw new ArgumentException ( message : " You must supply a name " , paramName : nameof (name));
return longRunningWorkImplementation ();
async Task < string > longRunningWorkImplementation ()
var interimResult = await FirstWork (address);
var secondResult = await SecondStep (index, name);
return $" The results are { interimResult } and { secondResult } . Enjoy. " ;
在使用异步函数前异常,不让开发者使用没有校验的 longRunningWorkImplementation ,这就是内部方法的使用。
但是可能有兄弟这样写,让我觉得这个语言太垃圾
改进常量
我们有比较长数字,那么我们在定义比较难知道他是否写对,这导致第一次阅读代码容易误解。特别是当数值没有规律的时候。C# 7.0包含两个新功能:二进制常量和数值分割。
C# 7.0现在做了改进,可以使用下划线,下划线可以分割常量。这样做的目的是看起来比较好看,容易数我们写了多少数字,可以看我们是不是写错。
例如对于二进制数值的定义
public const int One = 0b0001 ;
public const int Two = 0b0010 ;
public const int Four = 0b0100 ;
public const int Eight = 0b1000 ;
定义的开始是0b
这个字符,之后就是二进制的0、1两个字符组成的数值。
不仅可以定义二进制数值,还可以添加下划线,下划线可以分割字符数值,如下代码所示。
public const int Sixteen = 0b0001_0000;
public const int ThirtyTwo = 0b0010_0000;
public const int SixtyFour = 0b0100_0000;
public const int OneHundredTwentyEight = 0b1000_0000;
数值分割下划线可以出现在除了字符串开始的任何位置,如定义一个比较大的整数,可以这样写
public const long BillionsAndBillions = 100_000_000_000;
同样,数值分割下划线支持decimal, float 和 double,对于浮点数值,可以在小数点之后任意位置添加。
public const double AvogadroConstant = 6.022_140_857_747_474e23;
public const decimal GoldenRatio = 1.618_033_988_749_894_848_204_586_834_365_638_117_720_309_179M;
总之,你可以定义出你觉得比较容易阅读的数值。
我们还可以定义2进制,原来是无法定义,但是所有的二进制还是存储用int,不是一个类型。
var b = 0b1010_1011_1100_1101_1110_1111;
这个功能在 Iot 经常需要使用二进制,如果是以前,可以使用 true 和 false、byte互转,写起来反人类,现在用这个方法就比较简单。二进制容易写错,所以上面的功能,可能是因为二进制做的。
ref returns 返回值
我们返回的是引用,现在返回可以是值,我们返回数组中的一个值,那么修改这个值,因为放进引用,我们输出数组是修改的值
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);
WriteLine (array[ 4 ]); // 9
全部地方可以支持辣么大
以前支持辣么大的地方很少,关于辣么大,参见 https://docs.microsoft.com/en-us/dotnet/articles/csharp/lambda-expressions
现在可以在所有地方使用辣么大
// Expression-bodied constructor
public ExpressionMembersExample ( string label) = > this .Label = label;
// Expression-bodied get / set accessors.
set => this .label = value ?? " Default label " ;
在表达式扔异常
以前,异常是定义,不可以放在表达式,所以下面代码出错
_name = value ??throw new ArgumentException ();
不能通过判断 value 是空,抛出异常,可以看到我的代码红了
现在可以把异常放在表达式,下面代码在vs17可以运行
同时可以写在字段定义
private ConfigResource loadedConfig = LoadConfigResourceOrDefault () ??
throw new InvalidOperationException ( " Could not load config " );
广义异步返回类型
以前 Task<>
只能在方法使用,必须返回引用类型,也会出现新建线程或构造的性能问题,请看下面的代码,虽然已经有了cache,但是还是需要进入新的task。
private async Task < int > loadCache ()
现在可以使用 ValueTask<>
返回数值,减少构造Task的性能问题,在ValueTask可以判断是否已经缓存,如果有就直接返回,这个的主要用法是减少task的使用,但是不修改方法,还是异步。
public ValueTask < int > CachedFunc ()
return (cache) ? new ValueTask < int >(cacheResult) : new ValueTask < int >( loadCache ());
private bool cache = false ;
private async Task < int > loadCache ()
注意使用System.Threading.Tasks.Extension
这个方法可以直接把数值转ValueTask
虽然没有用,和之前的看不出有什么用,但是这个一个很大的工程。
public static async ValueTask < int > ValueTask ( int [] numbers )
return await Task. Run (() => numbers. Sum ());
上面代码其实无法体现出实际用处,在 http://www.cnblogs.com/GuZhenYin/p/6526041.html 博客,笑对当空 说 这样可以在高频调用异步时不再花费更多的线程重复运行结果
支持 async 的主函数
这个特性需要使用 7.1 是支持异步的主函数。
如果需要使用最新的,那么可以右击项目属性,点击高级,可以看到下面的页面
选择最新的版本
以前的命令行主函数都是使用 void ,现在支持使用 async
static async Task < int > Main ()
// This could also be replaced with the body
// DoAsyncWork, including its await expressions:
return await DoAsyncWork ();
如果 Main 不需要返回,那么可以直接使用下面代码
默认值不需要写类型
以前需要写一个类型的默认值,一般都是比较长的,如下面的代码
Func < string , bool > whereClause = default ( Func < string , bool >);
如果使用的是 ValueTuple ,那么看到的代码会比上面的长,但是 default 是可以在使用就知道类型,所以现在可以直接写 default 就好,请看下面
Func < string , bool > whereClause = default ;
感觉这个功能是 ValueTuple 很好用,大家都使用这个东西,然后发现这个不能直接给 null 所以需要写 default ,所以就必须弄这个东西,看来微软很注重用户体验。
自动推断 Tuple 的名称
以前的 ValueTuple 不能自动推断自己的名称,请看下面
string label = " Colors used in the map " ;
var pair = ( count : count, label : label);
虽然已经有了两个变量,但是需要自己写名称,新的就可以自己获得名称
string label = " Colors used in the map " ;
var pair = (count, label); // element names are "count" and "label"
值引用
现在支持使用 in 写在函数的参数,参数是让结构体可以直接传引用。以前如果在参数写结构体,那么结构体是复制的,如果一个软件有很多参数都是使用结构体,那么结构体就需要很多复制,于是这样的内存性能比较差。
现在支持下面的写法
private static double CalculateDistance2 ( in Point3D point1 , in Point3D point2 = default )
double xDifference = point1.X - point2.X;
double yDifference = point1.Y - point2.Y;
double zDifference = point1.Z - point2.Z;
return Math. Sqrt (xDifference * xDifference + yDifference * yDifference + zDifference * zDifference);
于是所有调用 CalculateDistance2 函数就不需要重新去复制 Point3D 的值,如果调用很多,那么就可以减少很多的内存,使用这个关键字就可以和引用类型一样,不需要复制值。
于是下面微软还用了更好的功能 ref readonly ,这是对属性和字段。如果有一个字段是 Origin 那么在使用他,用一个变量去拿,那么就会复制值,所以拿到的就和原来的不是一个对象,内存使用比较多。
现在可以使用下面代码,所有拿到的变量和原来都是一个值,不需要复制,虽然看起来没有优化很多,但是如果框架使用了,那么很多地方使用就可以看到减少很多内存。
private static Point3D _origin = new Point3D ();
public static ref readonly Point3D Origin => ref _origin;
var originValue = Point3D.Origin;
ref readonly var originReference = ref Point3D.Origin;
上面的 originValue 是原来的方法,这样还是需要复制,但是 originReference 是直接拿引用。
但是对于结构体,是不是拿到了引用就可以修改里面的属性?因为有很多智障的人都会去修改结构体的属性,所以微软就给一个结构体也使用这个方法,只能读
readonly public struct ReadonlyPoint3D
public ReadonlyPoint3D ( double x , double y , double z )
private static readonly ReadonlyPoint3D origin = new ReadonlyPoint3D ();
public static ref readonly ReadonlyPoint3D Origin => ref origin;
指定位置命名参数
很多人都不知道,方法可以指定命名,请看下面的代码
PrintOrderDetails ( " Gift Shop " , 31 , " Red Mug " );
PrintOrderDetails ( orderNum : 31 , productName : " Red Mug " , sellerName : " Gift Shop " );
但是写了命名参数,就不能就写其中的几个,如下面的代码在以前是无法编译,因为不能找到参数对应的
PrintOrderDetails ( sellerName : " Gift Shop " , 31 , productName : " Red Mug " );
但是现在可以编译通过了,但是需要写的位置和函数的一样,如上面的 31 如何和后面的名称位置换了,那么无法编译。
如果写了三个参数,其中两个用了命名,一个没有,为什么微软不自动找到位置?因为有一些重载是参数的位置不相同,如果使用自动判断,那么就不知道使用哪个函数,请看下面的代码
void PrintOrderDetails ( string sellerName , int orderNum , string productName )
void PrintOrderDetails ( string sellerName , string productName , int orderNum )
写了一个 PrintOrderDetails(31,sellerName: "Gift Shop", productName: "Red Mug")
那么是需要使用上面哪个函数
private protected 访问修饰
以前的 protected 只能指定在继承类可以使用,如果类在其他程序集,那么他还是可以使用,请看下面的代码
protected int myValue = 0 ;
public class DerivedClass1 : BaseClass
BaseClass baseObject = new BaseClass ();
// 下面代码出现错误,只有继承的类可以使用,在类外面不能使用
// baseObject.myValue = 5;
无论 DerivedClass1 是写在其他程序集都可以使用。
那么 protected internal 是做什么?
protected internal int myValue = 0 ;
BaseClass baseObject = new BaseClass ();
可以和上面的看,上面的代码可以直接使用baseObject.myValue
在相同的程序集外面可以直接使用这个属性。但是在其他程序集就不能这样写。
class DerivedClass : BaseClass
BaseClass baseObject = new BaseClass ();
DerivedClass derivedObject = new DerivedClass ();
// baseObject.myValue = 10;
derivedObject.myValue = 10 ;
那么 private protected 是做什么?
private protected int myValue = 0 ;
public class DerivedClass1 : BaseClass
BaseClass baseObject = new BaseClass ();
// baseObject.myValue = 5;
在其他程序集无法使用这个属性,无论使用的是继承这个类拿到属性
class DerivedClass2 : BaseClass
实际上这样写就是在其他的程序集无法访问这个属性,但是在一个程序级可以继承访问这个属性
推荐一个写很好的博客解析Visual C# 7.2中的private protected访问修饰符 - dax.net - 博客园
字符常量下划线可以放在最前
以前的字符常量不能使用下划线开始,现在可以使用下划线开始
https://docs.microsoft.com/en-us/dotnet/articles/csharp/csharp-7
https://blogs.msdn.microsoft.com/dotnet/2016/08/24/whats-new-in-csharp-7-0/
VS 17 下载
VS 17 企业版
链接:http://pan.baidu.com/s/1skXDc3z 密码:70d6
秘钥 Njvyc-bmhx2-g77mm-4xjmr-6q8qf
如果度盘链接没法使用,请联系我。
btsync:BTZR4YIPCLUUEL2BKDACVGLC3473MEWDN
代码 https://github.com/alugili/CSharp7Features
如果自己想下载离线包,可以到官网去下载一个应用,也就是在线安装版本。
然后使用 cmd 打开,假如我下载的是 vs_community.exe,那么我想安装语言为中文的包,包括常用开发,可以使用下面代码
vs_community.exe -- layout E :\vs2017离线 -- lang zh - CN -- add Component.GitHub.VisualStudio -- add Microsoft.VisualStudio.Component.CoreEditor -- add Microsoft.VisualStudio.Workload.ManagedDesktop
需要解释上面的代码
vs_community.exe -- layout 下载地址 -- lang 语言,如果有多个,使用空格 -- add 添加工作空间 -- add Microsoft.VisualStudio.Component.CoreEditor -- add Microsoft.VisualStudio.Workload.ManagedDesktop
语言参见:https://docs.microsoft.com/en-us/visualstudio/install/use-command-line-parameters-to-install-visual-studio#list-of-language-locales
工作空间可以到 https://docs.microsoft.com/en-us/visualstudio/install/workload-and-component-ids 查看,对不同的vs,需要使用不同的工作空间
Install on low bandwidth or unreliable network environments
原文链接: http://blog.lindexi.com/post/C-7.0
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。
欢迎转载、使用、重新发布,但务必保留文章署名 林德熙 (包含链接: https://blog.lindexi.com ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请与我 联系 。