Chaptor 1 . C# Language Basics
内容来自书籍《C# 10 in a Nutshell》
Author:Joseph Albahari
需要该电子书的小伙伴,可以留下邮箱,有空看到就会发送的
A First C# Program
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
类型,代表这个方法没有返回值
Compilation
C#的编译器会将源码编译到assembly.
,一个assembly
是包的集合。它可以是一个应用或者库,它们之间的不同是,应用是有一个入口的执行函数,但是库没有入口。
库的目的是被应用所引用或者被其他库引用。
每个程序的的都有一个被叫做top-level statements
的入口文件,这个文件会隐式创建一个入口函数,也就是常说的Main函数
dotnet
命令行工具,是可以用来管理.NET源码和二进制程序的,它可以构建程序,运行程序
// 创建一个Console程序
dotnet new Console -n MyFirstProgram
// 构建程序,然后运行这个程序
dotnet run MyFirstProgram
// 构建这个程序
dotnet build MyFirstProgram.csproj
需要注意的是,在执行dotnet
命令的时候,最好处于项目内部
Syntax
Identifiers and Keywords
using System;
int x = 12 * 30;
Console.WriteLine (x);
System, x, Console, WriteLine
,都是标识符,是开发者用来选择类、方法、变量等
标识符可以使用下划线、字母开头的Unicode
字符集,区分大小写,按照约定,参数、变量和私有字段应该用camel case
,而其他的所有标识符应该用Pascal case
Keywords
关键字,指的是对编译器来说,是有特殊含义的一些单词,这里使用到了using, int
,要注意的是,不能使用关键字作为标识符
如果真的希望使用关键字作为标识符,可以添加@
前缀给标识符
Comments
单行注释//
多行注释/* ... */
文档注释///
Type Basics
在C#中,所有的值都是类型的实例
Predefined Type Examples
预定义类型是编译器提供的特殊的类型。比如说基本数字类型int
,还有字符串类型string
,布尔值bool
Custom Types
public class UnitConverter
{
int ratio; // Field 字段
public UnitConverter(int unitRatio) // Constructor 构造函数
{
ratio = unitRatio;
}
public int Convert(int unit) // Method 方法
{
return unit * ratio;
}
}
The public keyword
如果标注了public
关键字,会将这个类型的成员暴露出来,如果没有标注访问权限,默认是private
的权限
Defining namespaces
namespace A
{
public class B
{
...
}
}
类型B的命名空间在A,在调用B的时候,需要带上new A.B()
,才能使用
Types and Conversions
两个合适的类型可以进行转换,可以进行隐式或者显式的自动转换
int x = 12345;
long y = x; // Implicit
short z = (short)x; // Explicit
Implicit的转换会发生在两种情况:
- 编译器可以保证这个转换总是成功的
- 没有信息会在转换中丢失
Explicit的转换只需要其中一种:
- 编译器不能保证转换是成功
- 信息可能在转换中丢失
Value Types Versus Reference Types
所有的c# 类型都可以区分为下面的几种类别中:
- Value types 值类型
- Reference types 引用类型
- Pointer types 指针类型
- Generic type parameters 范型类型参数
值类型一般是内建的基础类型,比如那些数字类型int
、long
,还有字符类型char
等,还有就是我们自定义的struct
类型和enum
类型也算是值类型
引用类型包括所有的class
、array
、delegate
和interface
类型(包括string
也是引用类型)
值类型和引用类型的不同之处在,它们处理内存的方式不同
Value types
值类型或者常量的内容,只是一个简单的值,比如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;
Reference types
引用类型要更加复杂,它有两个部分组成一个是对象,一个是指向对象的引用。
它的内存视图
在对引用类型做赋值动作的时候,只会cpoy
引用,而不是对象实例。这样就造成,可以同时有多个引用变量指向同一个对象。
Point p1 = new Point();
p1.X = 7;
Point p2 = p1;// Copies p1 reference
p1.X = 9;// Change p1.X
Null
一个引用类型,可以被赋值为null
,表明这个引用没有指向任何对象
Storage overhead
值类型占用的内存大小是刚刚好的,比如说结构体Point,它的占用大小是8bytes
struct Point
{
int x; // 4 bytes
int y; // 4 bytes
}
需要注意的是,这个大小是会有内存对齐的原因,而导致和你预想的不一样
struct A { byte b; long l; }
这个看起来可能是占用7bytes,但是实际是16bytes,因为会对齐最大的那个字段的大小
Specialized Operations on Integral Types
Overflow check operators
checked
操作符可以在运行时生成一个错误OverflowException
,这样比变量溢出了,但是没有任何反馈。
int c = checked (a * b);// 只是检查这个表达式
// 检查block内的代码
checked
{
...
c = a * b;
...
}
也可以解除这种检查,通过使用关键字unchecked
关于常量的计算溢出,是在编译时做检查,所以不会隐式溢出
8- and 16-Bit Integral Types
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
Strings and Characters
C#的char类型代表了一个Unicode
字符,占用2bytes(UTF-16)
String Type
C#的一个字符串代表一个不可变的Unicode字符序列
在字符串的前面添加@
,代表字符串的内容不做转义
String interpolation
插值字符串
int x = 4;
Console.Write ($"A square has {x} sides");
Arrays
char[] vowels = new char[5]; // 声明一个字符数组
char[] vowels = new char[] {'a','e','i','o','u'}; 声明并初始化一个字符数组
char[] vowels = {'a','e','i','o','u'};
Default Element Initialization
创建一个数组,总是会预初始化数组元素为默认值。默认值在元素是值类型或者引用类型是有性能影响的。
如果元素是值类型,那么每个元素都会作为元素的一部分初始化
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; }
Indices and Ranges
Indices
索引可以让你引用一个相对数组的末尾的元素,使用^
操作符。比如,^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
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'
Multidimensional Arrays
多维数组有两种形式: rectangular
和jagged
矩形数组表示 n 维内存块,锯齿数组是数组的数组。
Rectangular arrays
int[,] matrix = new int[3,3];
int[,] matrix = new int[,]
{
{0,1,2},
{3,4,5},
{6,7,8}
};
Jagged arrays
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,然后内部的数组可以每一个的长度都不一致
Simplified Array Initialization Expressions
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}
};
Bounds Checking
在运行时会对数组做边界检查
Variables and Parameters
一个变量代表的是一个可变的存储位置,它可以是一个本地变量、参数(value,ref,out,in)、字段(实例、静态),或者是一个数组的元素
The Stack and the Heap
栈和堆是存储变量的地方,它们有着不同的生命周期
Stack
栈是用来存储本地变量和参数的。随着方法或者函数的进入和退出,栈会逻辑地增长和收缩。
Heap
堆是对象(即是引用类型)的驻留内存。每当一个对象创建,就会在堆中申请内存,并有一个引用指向这个对象。运行时有垃圾收集器来将对象的占用内存释放。当对象不再被引用之后,就会被垃圾收集器回收
Default Values
可以用关键字default
获取任何类型的默认值
Console.WriteLine (default (decimal));
decimal d = default;
Parameters
可以控制参数传递的额外的行为
Passing arguments by value
在默认情况下,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;
}
The ref modifier
参数传递的时候,添加了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
}
The out modifier
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);
}
The in modifier
in
关键字也是和ref
差不多,唯一区别是,in
修饰的参数,在方法中是不可变的,如果改了,会有编译错误。
The params modifier
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;
}
Optional parameters
可选参数,在方法声明中已经给定某个值
Foo();
Foo (23);
void Foo (int x = 23) { Console.WriteLine (x); }
可选参数,不可以用ref
或者out
修饰符
Named arguments
Foo (x:1, y:2); // 1, 2
void Foo (int x, int y) { Console.WriteLine (x + ", " + y); }
Ref Locals
int[] numbers = { 0, 1, 2, 3, 4 };
ref int numRef = ref numbers [2];
numRef是numbers[2]的引用,修改numRef,会影响到numbers[2]
而且,Ref locals只能应用在数组元素、字段、或者本地变量,不能应用在属性上
一般是和ref returns一起使用
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;
Target-Typed new Expressions
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");
Expressions and Operators
Operator Table
Null Operators
C#提供了三种操作符让操作null值更简单:null-coalescing
、null-coalescing assignment
、null-conditional
Null-Coalescing Operator
??
操作符。语义是“如果左边的操作数不是null,将它给我;否则,给我另外一个值(右操作数)”
string s1 = null;
string s2 = s1 ?? "nothing"; // s2 evaluates to "nothing"
它同样适用于nullable value types
,可空值类型
Null-Coalescing Assignment Operator
??=
操作符。它的语义是“如果左操作数是null,那就将右操作数赋值给左操作数”
myVariable ??= someDefault;
这个操作符特别适用于延迟加载的属性
Null-Conditional Operator
?.
操作符。它允许你在调用方法或者访问成员时,和正常的dot操作符一样,除了当你的左操作数是null的时候,整个表达式的结果是null,而不是抛出异常NullReferenceException
System.Text.StringBuilder sb = null;
string s = sb?.ToString(); // No error; s instead evaluates to null
同样还有个索引访问的时候,也可以使用?[]
Statements
Selection Statements
The switch statement
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 expressions
switch表达式,是有计算结果的,可以作为右值赋值给左值
string cardName = cardNumber switch
{
13 => "King",
12 => "Queen",
11 => "Jack",
_ => "Pip card" // equivalent to 'default'
};
Iteration Statements
foreach loops
对一个enumerable
对象迭代。
foreach (char c in "beer")
Console.WriteLine (c);
Miscellaneous Statements
using
语句,为IDisposable
对象在finally
block里面调用Dispose
方法
lock
语句是一个是调用 Monitor 类的 Enter 和 Exit 方法的快捷方式
Namespaces
命名空间是类型名称的域。类型通常组织为分层命名空间,这样容易防止命名冲突
类型就包裹在namespace
block下
namespace Outer.Middle.Inner
{
class Class1 {}
class Class2 {}
}
等价于
namespace Outer
{
namespace Middle
{
namespace Inner
{
class Class1 {}
class Class2 {}
}
}
}
using static
导入了这个类型的所有公开静态成员
Rules Within a Namespace
声明在外部的命名空间,可以不需要导入就能让它的内部的命名空间访问
namespace Outer
{
class Class1 {}
namespace Inner
{
class Class2 : Class1
{}
}
}
Aliasing Types and Namespaces
起别名
using PropertyInfo2 = System.Reflection.PropertyInfo;
Advanced Namespace Features
Extern
当程序引用的两个库,都有相同的命名空间和类型名称时,可通过以下方式解决
<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();
原创文章,作者:506227337,如若转载,请注明出处:https://blog.ytso.com/271155.html