内容来自书籍《C# 10 in a Nutshell》
Author:Joseph Albahari
需要该电子书的小伙伴,可以留下邮箱,有空看到就会发送的
int x = 12 * 30; System.Console.WriteLine (x);
计算 12 * 30
的结果,然后存储在变量x
中,因为C#是强类型的语言,所以所有变量的类型都必须是编译期间已知
然后Console
类的静态方法WriteLine
,前面的System
是命名空间,我们也可以用using System
来导入这个命名空间下的所有公开的API,这样就不需要频繁编写前缀了Console.WriteLine (x);
using System; Console.WriteLine (FeetToInches (100)); int FeetToInches (int feet) { int inches = feet * 12; return inches; }
方法的声明,前面是返回值类型,方法名,入参的形参变量类型和名称。方法体是用一个大括号包裹着。和Java
的不一样,它的大括号是都在方法签名的下方,Java
的是有一个大括号在方法签名的末尾。如果方法没有返回值,那么应该使用void
类型,代表这个方法没有返回值
C#的编译器会将源码编译到assembly.
,一个assembly
是包的集合。它可以是一个应用或者库,它们之间的不同是,应用是有一个入口的执行函数,但是库没有入口。
库的目的是被应用所引用或者被其他库引用。
每个程序的的都有一个被叫做top-level statements
的入口文件,这个文件会隐式创建一个入口函数,也就是常说的Main函数
dotnet
命令行工具,是可以用来管理.NET源码和二进制程序的,它可以构建程序,运行程序
// 创建一个Console程序 dotnet new Console -n MyFirstProgram // 构建程序,然后运行这个程序 dotnet run MyFirstProgram // 构建这个程序 dotnet build MyFirstProgram.csproj
需要注意的是,在执行dotnet
命令的时候,最好处于项目内部
using System; int x = 12 * 30; Console.WriteLine (x);
System, x, Console, WriteLine
,都是标识符,是开发者用来选择类、方法、变量等
标识符可以使用下划线、字母开头的Unicode
字符集,区分大小写,按照约定,参数、变量和私有字段应该用camel case
,而其他的所有标识符应该用Pascal case
Keywords
关键字,指的是对编译器来说,是有特殊含义的一些单词,这里使用到了using, int
,要注意的是,不能使用关键字作为标识符
如果真的希望使用关键字作为标识符,可以添加@
前缀给标识符
单行注释//
多行注释/* ... */
文档注释///
在C#中,所有的值都是类型的实例
预定义类型是编译器提供的特殊的类型。比如说基本数字类型int
,还有字符串类型string
,布尔值bool
public class UnitConverter { int ratio; // Field 字段 public UnitConverter(int unitRatio) // Constructor 构造函数 { ratio = unitRatio; } public int Convert(int unit) // Method 方法 { return unit * ratio; } }
如果标注了public
关键字,会将这个类型的成员暴露出来,如果没有标注访问权限,默认是private
的权限
namespace A { public class B { ... } }
类型B的命名空间在A,在调用B的时候,需要带上new A.B()
,才能使用
两个合适的类型可以进行转换,可以进行隐式或者显式的自动转换
int x = 12345; long y = x; // Implicit short z = (short)x; // Explicit
Implicit的转换会发生在两种情况:
Explicit的转换只需要其中一种:
所有的c# 类型都可以区分为下面的几种类别中:
值类型一般是内建的基础类型,比如那些数字类型int
、long
,还有字符类型char
等,还有就是我们自定义的struct
类型和enum
类型也算是值类型
引用类型包括所有的class
、array
、delegate
和interface
类型(包括string
也是引用类型)
值类型和引用类型的不同之处在,它们处理内存的方式不同
值类型或者常量的内容,只是一个简单的值,比如int内置类型,它是一个32bit的数据,也可以自定义struct类型
public struct Point { public int X, Y; }
它的内存视图:
值类型的实例在赋值的时候,总是copy
的行为
Point p1 = new Point(); p1.X = 7; Point p2 = p1;// Assignment causes copy p1.X = 9;
引用类型要更加复杂,它有两个部分组成一个是对象,一个是指向对象的引用。
它的内存视图
在对引用类型做赋值动作的时候,只会cpoy
引用,而不是对象实例。这样就造成,可以同时有多个引用变量指向同一个对象。
Point p1 = new Point(); p1.X = 7; Point p2 = p1;// Copies p1 reference p1.X = 9;// Change p1.X
一个引用类型,可以被赋值为null
,表明这个引用没有指向任何对象
值类型占用的内存大小是刚刚好的,比如说结构体Point,它的占用大小是8bytes
struct Point { int x; // 4 bytes int y; // 4 bytes }
需要注意的是,这个大小是会有内存对齐的原因,而导致和你预想的不一样
struct A { byte b; long l; }
这个看起来可能是占用7bytes,但是实际是16bytes,因为会对齐最大的那个字段的大小
checked
操作符可以在运行时生成一个错误OverflowException
,这样比变量溢出了,但是没有任何反馈。
int c = checked (a * b);// 只是检查这个表达式 // 检查block内的代码 checked { ... c = a * b; ... }
也可以解除这种检查,通过使用关键字unchecked
关于常量的计算溢出,是在编译时做检查,所以不会隐式溢出
8bit和16bit的整数类型,有byte、sbyte、short和ushort,这些类型的计算操作,C#会隐式地转换为更大的类型,而这会造成一个编译时错误
short x = 1, y = 1; short z = x + y; // Compile-time error short z = (short) (x + y); // OK
C#的char类型代表了一个Unicode
字符,占用2bytes(UTF-16)
C#的一个字符串代表一个不可变的Unicode字符序列
在字符串的前面添加@
,代表字符串的内容不做转义
插值字符串
int x = 4; Console.Write ($"A square has {x} sides");
char[] vowels = new char[5]; // 声明一个字符数组 char[] vowels = new char[] {'a','e','i','o','u'}; 声明并初始化一个字符数组 char[] vowels = {'a','e','i','o','u'};
创建一个数组,总是会预初始化数组元素为默认值。默认值在元素是值类型或者引用类型是有性能影响的。
如果元素是值类型,那么每个元素都会作为元素的一部分初始化
Point[] a = new Point[1000]; int x = a[500].X; // 0 public struct Point { public int X, Y; }
如果是引用类型,那么元素会是null
Point[] a = new Point[1000]; int x = a[500].X; // Runtime error, NullReferenceException public class Point { public int X, Y; }
索引可以让你引用一个相对数组的末尾的元素,使用^
操作符。比如,^1
就是获取到数组的最后一个元素,^2
是数组的倒数第二个元素
char[] vowels = new char[] {'a','e','i','o','u'}; char lastElement = vowels [^1]; // 'u' char secondToLast = vowels [^2]; // 'o'
这个操作符其实结果是一个Index
类型
Index first = 0; Index last = ^1; char firstElement = vowels [first]; // 'a' char lastElement = vowels [last]; // 'u'
Ranges
让你可以对一个数组切片,用..
操作符
char[] firstTwo = vowels [..2]; // 'a', 'e' char[] lastThree = vowels [2..]; // 'i', 'o', 'u' char[] middleOne = vowels [2..3]; // 'i'
操作符的结果是一个类型Range
Range firstTwoRange = 0..2; char[] firstTwo = vowels [firstTwoRange]; // 'a', 'e'
多维数组有两种形式: rectangular
和jagged
矩形数组表示 n 维内存块,锯齿数组是数组的数组。
int[,] matrix = new int[3,3]; int[,] matrix = new int[,] { {0,1,2}, {3,4,5}, {6,7,8} };
int[][] matrix = new int[3][]; int[][] matrix = new int[][] { new int[] {0,1,2}, new int[] {3,4,5}, new int[] {6,7,8,9} };
它是数组的数组,所以在声明的时候,可以声明第一个数组,内部元素都是null,然后内部的数组可以每一个的长度都不一致
char[] vowels = {'a','e','i','o','u'}; int[,] rectangularMatrix = { {0,1,2}, {3,4,5}, {6,7,8} }; int[][] jaggedMatrix = { new int[] {0,1,2}, new int[] {3,4,5}, new int[] {6,7,8,9} }; var vowels = new[] {'a','e','i','o','u'}; var rectMatrix = new int[,] { {0,1,2}, {3,4,5}, {6,7,8} }; var jaggedMat = new int[][] { new int[] {0,1,2}, new int[] {3,4,5}, new int[] {6,7,8,9} };
在运行时会对数组做边界检查
一个变量代表的是一个可变的存储位置,它可以是一个本地变量、参数(value,ref,out,in)、字段(实例、静态),或者是一个数组的元素
栈和堆是存储变量的地方,它们有着不同的生命周期
栈是用来存储本地变量和参数的。随着方法或者函数的进入和退出,栈会逻辑地增长和收缩。
堆是对象(即是引用类型)的驻留内存。每当一个对象创建,就会在堆中申请内存,并有一个引用指向这个对象。运行时有垃圾收集器来将对象的占用内存释放。当对象不再被引用之后,就会被垃圾收集器回收
可以用关键字default
获取任何类型的默认值
Console.WriteLine (default (decimal)); decimal d = default;
可以控制参数传递的额外的行为
在默认情况下,C#是值传递,这意味着会copy
一个值传递到方法内部,所以在方法的内部,对参数的修改,不会影响到方法外部的变量;但是如果将一个引用使用值传递的方式传递到方法,那么会变成两个变量指向一个对象,所以在方法内部对这个对象的修改,会影响到外部的变量,而如果是给这个引用参数赋值,其实是将这个引用指向其他的对象,对外部变量没有影响。
int x = 8; Foo (x); Console.WriteLine (x); static void Foo (int p) { p = p + 1; Console.WriteLine (p); }
StringBuilder sb = new StringBuilder(); Foo (sb); Console.WriteLine (sb.ToString()); // test static void Foo (StringBuilder fooSB) { fooSB.Append ("test"); fooSB = null; }
参数传递的时候,添加了ref
关键字在方法签名中,这相当于给实参起了一个别名,它们同时指向同一个内存地址,或者说一个内存地址有两个变量名。注意的是,ref
是传递方和声明方都要写
int x = 8; Foo (ref x); // Ask Foo to deal directly with x Console.WriteLine (x); // x is now 9 static void Foo (ref int p) { p = p + 1; // Increment p by 1 Console.WriteLine (p); // Write p to screen }
out
关键字和ref
差不多,除了:
Split ("Stevie Ray Vaughan", out string a, out string b); void Split (string name, out string firstNames, out string lastName) { int i = name.LastIndexOf (' '); firstNames = name.Substring (0, i); lastName = name.Substring (i + 1); }
in
关键字也是和ref
差不多,唯一区别是,in
修饰的参数,在方法中是不可变的,如果改了,会有编译错误。
params
关键字是用在方法的最后一个参数的修饰符。它允许方法接收指定类型的多个参数。参数的类型必须是数组形式的
int total = Sum (1, 2, 3, 4); int Sum (params int[] ints) { int sum = 0; for (int i = 0; i < ints.Length; i++) sum += ints [i]; return sum; }
可选参数,在方法声明中已经给定某个值
Foo(); Foo (23); void Foo (int x = 23) { Console.WriteLine (x); }
可选参数,不可以用ref
或者out
修饰符
Foo (x:1, y:2); // 1, 2 void Foo (int x, int y) { Console.WriteLine (x + ", " + y); }
int[] numbers = { 0, 1, 2, 3, 4 }; ref int numRef = ref numbers [2];
numRef是numbers[2]的引用,修改numRef,会影响到numbers[2]
而且,Ref locals只能应用在数组元素、字段、或者本地变量,不能应用在属性上
一般是和ref returns一起使用
可以在方法中返回一个Ref locals,这就是ref returns
static string x = "Old Value"; static ref string GetX() => ref x; static void Main() { ref string xRef = ref GetX(); xRef = "New Value"; Console.WriteLine (x);` }
// 这也是合法的,只是方法返回值不是ref的 string localX = GetX(); // Legal: localX is an ordinary non-ref variable.
// 可以在返回值添加只读限制 static ref readonly string Prop => ref x;
System.Text.StringBuilder sb1 = new(); System.Text.StringBuilder sb2 = new ("Test"); 等价于 System.Text.StringBuilder sb1 = new System.Text.StringBuilder(); System.Text.StringBuilder sb2 = new System.Text.StringBuilder ("Test");
C#提供了三种操作符让操作null值更简单:null-coalescing
、null-coalescing assignment
、null-conditional
??
操作符。语义是“如果左边的操作数不是null,将它给我;否则,给我另外一个值(右操作数)”
string s1 = null; string s2 = s1 ?? "nothing"; // s2 evaluates to "nothing"
它同样适用于nullable value types
,可空值类型
??=
操作符。它的语义是“如果左操作数是null,那就将右操作数赋值给左操作数”
myVariable ??= someDefault;
这个操作符特别适用于延迟加载的属性
?.
操作符。它允许你在调用方法或者访问成员时,和正常的dot操作符一样,除了当你的左操作数是null的时候,整个表达式的结果是null,而不是抛出异常NullReferenceException
System.Text.StringBuilder sb = null; string s = sb?.ToString(); // No error; s instead evaluates to null
同样还有个索引访问的时候,也可以使用?[]
switch语句,语句是没有结果的
switch (cardNumber) { case 13: Console.WriteLine ("King"); break; case 12: Console.WriteLine ("Queen"); break; case 11: Console.WriteLine ("Jack"); break; default: Console.WriteLine (cardNumber); break; }
还可以匹配类型
switch (x) { case int i: Console.WriteLine ("It's an int!"); Console.WriteLine ($"The square of {i} is {i * i}"); break; case string s: Console.WriteLine ("It's a string"); Console.WriteLine ($"The length of {s} is {s.Length}"); break; default: Console.WriteLine ("I don't know what x is"); break; }
switch表达式,是有计算结果的,可以作为右值赋值给左值
string cardName = cardNumber switch { 13 => "King", 12 => "Queen", 11 => "Jack", _ => "Pip card" // equivalent to 'default' };
对一个enumerable
对象迭代。
foreach (char c in "beer") Console.WriteLine (c);
using
语句,为IDisposable
对象在finally
block里面调用Dispose
方法
lock
语句是一个是调用 Monitor 类的 Enter 和 Exit 方法的快捷方式
命名空间是类型名称的域。类型通常组织为分层命名空间,这样容易防止命名冲突
类型就包裹在namespace
block下
namespace Outer.Middle.Inner { class Class1 {} class Class2 {} } 等价于 namespace Outer { namespace Middle { namespace Inner { class Class1 {} class Class2 {} } } }
导入了这个类型的所有公开静态成员
声明在外部的命名空间,可以不需要导入就能让它的内部的命名空间访问
namespace Outer { class Class1 {} namespace Inner { class Class2 : Class1 {} } }
起别名
using PropertyInfo2 = System.Reflection.PropertyInfo;
当程序引用的两个库,都有相同的命名空间和类型名称时,可通过以下方式解决
<ItemGroup> <Reference Include="Widgets1"> <Aliases>W1</Aliases> </Reference> <Reference Include="Widgets2"> <Aliases>W2</Aliases> </Reference> </ItemGroup>
extern alias W1; extern alias W2; W1.Widgets.Widget w1 = new W1.Widgets.Widget(); W2.Widgets.Widget w2 = new W2.Widgets.Widget();