目录
目录
1. C#的最新版本应该是7.0,每一个版本都会有一个焦点特性
由于工作需要,最近要学C#,所以选了一门入门较快的书籍《C#图解教程》,虽然之前有C++和python的基础,但是C#还是有很多不同的特性,所以在此记录如下,以备自己日后查阅
第一章
1. C#的最新版本应该是7.0,每一个版本都会有一个焦点特性
版本 | 焦点特性 | 其他特性 |
7.0 | 语法 | out变量 模式匹配 元组 解构 局部函数 数字分隔符 二进制文字 局部引用和引用返回 扩展异步返回类型 表达式的构造函数和finalizers Expression bodied getters and settersthrow表达式 |
6.0 | 语法 | Compiler-as-a-service(Roslyn) 将静态类型成员导入命名空间 异常过滤器 在Catch和Finally中使用Await 自动属性初始化器 只读属性的默认值 Expression-bodied members Null-conditional operators(空条件运算符,简洁检查) 字符串插值 nameof operator字典初始化器 |
5.0 | 异步 | Caller info attributes |
4.0 | 命名参数和可选参数 | 动态绑定 命名和可选参数 Generic co- and contravariance 嵌入式互操作类型(“NoPIA”) |
3.0 | LINQ | 隐式类型局部变量 对象和收集初始化器 自动实现的属性 匿名类型 扩展方法 查询表达式 Lambda表达式 表达树 部分方法 |
2.0 | 泛型 | 部分类型 匿名方法 迭代器 可空类型 Getter / setter单独可访问性 方法组转换(代表) Co- and Contra-variance for delegates 静态类 Delegate inference |
1.0 | C# | 基本框架 |
了解C#版本的发展对于学习C#也是有一定帮助的。
第二章
1. 格式字符串
Write语句和WriteLine语句的常规形式中可以有一个以上的参数。如果参数不止一个,参数用逗号分隔,第一个参数必须总是
字符串,称为格式字符串。格式字符串可以包含替代标记。
- 替代标记在格式字符串中标出位置,在输出串中该位置用一个值来替代
- 替代标记有一个整数即扩住它的大括号组成,其中整数就是替换值数字位置,跟着格式字符串的参数称为替换值,这些替换值是从0开始的
语法如下:
Console.WriteLine(格式字符串(含替代标记),替换值0,替换值1,……)
示例如下:
Console.WriteLine("Two sample integers are {0} and {1}",2,4);
//输出结果为:
Two sample integers are 2 and 4
格式化字符串还有很多内定的格式,如货币,百分比,时间等等,可参看具体的说明文档。
第三章
1.预定义类型
C#中一共有16种预定义类型,其中有三种非简单类型如下:
- string ,它是一个Unicode字符数组
- object, 它是所有其它类型的基类
- dynamic, 使用动态语言编写的程序时使用
所有类型关系如下:
2.用户定义类型
还有6种类型用户可以自己创建,如下
类类型 | class |
结构类型 | struct |
数组结果 | array |
枚举类型 | enum |
委托类型 | delegate |
接口类型 | interface |
类型通过类型声明创建,类型声明包含以下信息:
- 要创建的类型的种类
- 新类型的名称
- 对类型中每个成员的声明,array和delegate除外。
一旦声明了类型,就可以创建和使用这种类型对象,就像它们时预定义的类型一样。
3.自动初始化和多变量声明
一些变量如果在声明时没有初始化,那么会被自动设置为默认值,一些则不能。没有自动初始化的默认值的变量在程序给它赋值之前包含为定义值。
变量类型 | 存储位置 | 自动初始化 | 用途 |
本地变量 | 栈或者栈和堆 | 否 | 用于函数成员内部计算 |
类字段 | 堆 | 是 | 类的成员 |
结构字段 | 栈或堆 | 是 | 结构的成员 |
参数 | 栈 | 否 | 用于将值传入或传出的方法 |
数组元素 | 堆 | 是 | 数组的成员 |
C#种可以把多个变量声明在一条单独的声明语句中
- 多变量声明中的变量必须类型相同
- 变量名必须用逗号分隔,可以在变量名后包含初始化语句
int var3=7,var4,var5=3;
double var5, var8=89;
第四章
无
第五章
1.类型推断和var关键字
有时候编译器能从初始化右边的语句推断出类型的信息,考虑如下代码:
static void main()
{
int total=15;
MyExcellentClass mec=new MyExcellentClass();
}
在上面的代码中,第一个变量的声明,编译器能推断出15是int类型,第二条语句返回一个MyExcellentClass类型,所以在以上两种情况下,声明中包含显示的类型名是多余的,因此C#提供了关键字var,是的上面的语句可以改为:
static void main()
{
var total=15;
var mec=new MyExcellentClass();
}
使用var关键字又一些重要条件:
- 只能用于本地变量,不能用于字段
- 只能在变量声明中包含初始化时使用
- 一旦编译器推断出变量的类型,他就是固定且不能改变的
2.引用类型作为值参数和引用参数
对于一个引用类型对象,不管是将其作为值参数还是引用参数,我们都可以在方法成员被捕修改它的成员,若在方法内部设置型参本身,则会有2一些区别。
- 将引用类型对象作为值参数传递 如果在方法内创建一个新对象并赋值给型参,将切断型参和实参的关联,并且在方法结束之后,新对象也将不复存在
- 将引用对象作为引用参数传递 如果在方法内创建一个新对象并赋值给形参,在方法结束后该对象依然存在,并且是实参所引用的值。
下面依次展示两种情况:
class MyClass { public int Val=20;}
class Program
{
static void RefAsParameter(MyClass f1)
{
f1.Val=50;
Console.WriteLine("After member assignment: {0}",f1.Val);
f1=new MyClass();
Console.WriteLine("After new Object creation: {0}",f1.Val);
}
Static void main()
{
MyClass a1=new MyClass();
Console.WriteLine("Before method call: {0}", a1.Val);
RefAsParameter(a1);
Console.WriteLine("After method call: {0}",a1.Val);
}
}
输出如下:
Before method call: 20
After member assignment: 50
After new Object creation: 20
After method call: 50
之所以会出现这种情况是因为:
- 在方法开始时,实参和形参都指向堆中相同的对象
- 在为对象成员赋值之后,它们仍指向堆中相同的对象
- 当方法分配新的对象并赋值给实参时,实参仍指向原始对象,而形参指向新对象
- 在调用方法之后,实参指向原始对象,形参和新对象消失
可以用如下图来解释:
而当应用类型对象作为引用参数时,则发生了一些变化,代码如下:主要是对于静态方法的参数加了ref,
class MyClass { public int Val=20;}
class Program
{
static void RefAsParameter(ref MyClass f1)
{
f1.Val=50;
Console.WriteLine("After member assignment: {0}",f1.Val);
f1=new MyClass();
Console.WriteLine("After new Object creation: {0}",f1.Val);
}
Static void main()
{
MyClass a1=new MyClass();
Console.WriteLine("Before method call: {0}", a1.Val);
RefAsParameter(a1);
Console.WriteLine("After method call: {0}",a1.Val);
}
}
运行结果如下:
同样:注意以下几点
- 在方法调用时,形参和实参都指向堆中相同的对象
- 对成员值的修改会同时影响到形参和实参
- 当方法创建新的对象并赋值给形参时,形参和实参的引用都指向该对象
- 在方法结束后,实参指向在方法内创建的新对象
图解如下:
其实上述两种调用的本质差别在于,作为值传递时,实参和形参是两个对象,只是一开始形参和实参指向的位置相同,而作为引用参数传递时,传递的就是实参本身,整个过程都只有一个对象。
3.传出参数
输出参数用于从方法体内把数据传出到调用代码,它们的行为与引用参数特别类似,如同引用参数,输出参数有以下要求:
- 必须在声明和调用中都使用修饰符out
- 实参必须是变量,而不能是其它类型表达式,因为方法需要内存保存返回值
示例如下:
void MyMethod(out int value)
{
...
}
int y;
MyMethod(out y);
与引用参数类似,输出参数的形参担当实参的别名,实参和形参都是同一块位置的别名,但与引用参数不同在于:
- 在方法内部,输出参数在能够被读取之前必须被复制,这意味着参数的初始值是无关的,而且没有必要在方法调用之前为实参赋值
- 在方法返回之前,方法内部贯穿的任何路径都必须为所输出的参数赋一次值
如果方法中有任何执行路径试图在输出参数赋值之前读取他,编译起就会产生错误信息
public void Add2(out int value)
{
int x=value+2;//出错,在赋值之前无法读取
}
4.参数数组
参数数组允许领个或多个实参对应一个特殊的型参。主要内容为:
- 在一个参数列表中,只能有一个参数数组
- 若果有,它必须是参数的最后一个
- 由参数数组表示的所有参数都必须具有相同的类型
- 声明一个参数数组,需要在那数据类型前加params关键字
- 在数据类型后放置一组空的方括号
一个包含int型参数数组的例子如下:
void ListInts(params int[] values)
{
...
}
而方法的调用方式可以有:
- 一个逗号分隔的该数据类型元素列表,所有元素的类型一致且和参数类型相同
ListInts(10,20,30);
- 一个该数据类型原属的一位数组
int[] intArray={1,2,3};
ListInts(intArray);
在使用一个参数数组时,编译器如下运作:
- 接受实参列表,用他们在堆中创建并初始化一个数组
- 把数组的引用保存到栈中的形参里
- 如果对一个的形参数组的位置没有实参,编译器会创建一个有0个元素的数组来使用
5.命名参数
前面所提到的参数都是位置参数,也就是实参和形参的位置一一对应,但和python中的命名参数一样,C#也1命名参数,只要显示的指定参数的名字,就可以以任意顺序调用这些实参。
class Myclass
{
public int Calc(int a, int b, int c)
{
return (a+b)*c;
}
static void main()
{
MyClass mc=new MyClass();
int result=mc.Calc(c:2,a:4,b:3);
Console.WriteLine("{0}",result);
}
}
在调用参数时,你既可以适应位置参数,也可以使用命名参数,但如果这么做,那么位置参数必须先列出。
第六章
1.readonly修饰符
字段可以用readonly修饰符声明,3作用类似于将字段声明为const,一旦值被设定就不能改变。
- const字段只能在字段的声明语句中初始化,而readonly字段可以在下列2种情况设置他的值
1. 字段声明语句
2. 类的任何构造函数
- const字段的1必须在编译时决定,而readonly字段可以在运行时决定
- 和const不同,const的行为总是静态的,而对于readonly字段,以下两点时正确的
1.它可以是实例字段,也可以是静态字段
2.它在内存中有存储位置
class Shape
{
readonly double PI=3.1416;
readonly int Num;
//在构造函数在设定Num
public Shape(double side1,double side2)
{
Num=4;//表示一个矩形
}
public Shape(double side1,double side2)
{
Num=3;//表示一个三角形
}
}
2.分部类与分部方法
类的声明可以分成几个分布类的声明,每个分部类的声明都含有一些类成员的声明,类的分部类声明可以在同一文件中也可以在不同文件中。每个局部声明必须被partial class,而不是当读的关键字class。
同样,分部方法是声明在分部类中不同部分的方法,分部方法的不同部分可以声明在不同的分部类中,也可以声明在同一个类中。分部方法的两个分部如下:
-
定义分部方法声明
a 给出签名和返回类型
b 声明的实现部分只是一个封号
2. 实现分部方法声明
a 给出签名和返回类型
b 以正常2的语句快实现
其中还有几个注意点:
- 返回类型必须是void
- 签名不能包括访问修饰符,使得分部方法是隐私私有的
- 参数列表不能包含out参数
- 在定义声明和实现声明中都必须包含上下文关键字
partial class MyClass
{
partial void PrintSum(int x, int y);
partial void Add(int x, int y)
{
PrintSum(x, y);
}
}
partial class MyClass
{
partial void PrintSum(int x, int y)
{
Console.Write("Sum is {0}",x+y);
}
}
class Program
{
static void main()
{
var mc=new MyClass();
mc.Add(5,6);
}
}
第七章
1. 屏蔽基类成员(关键字new)
虽然派生类不能删除它继承的任何成员,但是可以用与积累成员相同名称的成员来屏蔽基类成员,这是继承的主要功能之一。要屏蔽一个基类成员,需要声明一个相同类型的的成员,并使用相同的名称。同样,要隐藏一个函数成员,可以声明一个带标签的函数成员(函数名,参数类型,参数数目相同,返回值可不同);要让编译器知道你在屏蔽一个基类成员,需要使用new修饰符,否则编译器会警告你隐藏了一个继承的成员。
class SomeClass
{
public string Field1="abc";
}
class OtherClass: SomeClass
{
new public string Field1="dfgh";
public void PrintField1()
{
Console.WriteLine(Field1);
Console.WriteLine(base.Field1);
}
}
class Program
{
static void main()
{
otherClass oc=new OtherClass();
oc.PrintField1();
}
}
2.抽象类与密封类
抽象类1指被设计为被继承的类,只能作为其他类的基类,因此不能创建抽象类的实例。抽象类需要使用关键字abstract,这个和java类似。
-
抽象类可以包含抽象成员或者普通的非抽象成员
-
抽象类也可以是派生自另外一个抽象类
-
任何派生自抽象类的类必须使用override关键字来实现该类所有的抽象成员,除非派生类自己也是抽象类。
密封类与抽象类相反,它不能作为基类,在java中可以用final关键字来实现,C#类似,使用sealed关键字来实现。
3.命名约定
编写程序时会定义很多变量类型,在.net中常用的命名类型以及频繁程度如下:
风格名称 | 描述 | 推荐使用 | 示例 |
Pascal大小写 | 标志符1每个单词的首字母大写 | 用于类型名称和类中对外可见的类成员名称。涉及的名称包括类、方法、命名空间、属性和公共字段 | CardDesk, DealerHand |
Camel大小写 | 标志符中每个单词的首字母大写,第一个单词的首字母除外 | 用于局部变量的名称和方法的形参名称 | totalCycleCount |
下划线加Camel大小写 | 以下划线开头的Camel大小写标志符 | 用于私有和受保护的字段 | _cycleCount, -selectedIndex |
第八章
1.运算符重载
运算符重载允许你定义C#运算符应该如何操纵自定义类型的操作数。
- 运算符重载只能用于类和结构
- 为类1结构重载一个运算符x,可以声明一个名称为opeartor x的方法并实现它的行为
- 一元运算符重载带一个单独的class或struct类型参数
- 二元运算符带2个参数,其中至少有一个参数是struct或class类型
public static LimitedInt operator -(LimitedInt x)
public static LimitedInt operator +(LimitedInt x, double y)
- 重载方法必须同时使用public static修饰符
- 运算符必须是要操作的类或结构的成员
当然不是所有的运算符都能重载,重载的类型也有限制(比C++中可重载的要少),此外,重载运算符不能做以下事情:
- 创建新的运算符
- 改变运算符的语法
- 重新定义运算符如何处理预定义类型
- 改变运算符的优先级和结合性
2.typeof运算符
typeof运算符,返回其参数的任何System.type对象,通过这个对象可以了解类型的特征。typeof是一个一元运算符,但是你不能重载它。typeof的使用示例如下
Type t=typeof(SomeClass); //Type是System命名空间的一个类
下面给出一个typeof的具体用法
using System.Reflection;
class SomeClass
{
public int field1;
public int field2;
public void Method1() {}
public int Method2() { return 1; }
}
class Program
{
static void Main()
{
System.Type type = typeof(SomeClass);
FieldInfo[] fieldInfos = type.GetFields();
MethodInfo[] methodInfos = type.GetMethods();
foreach(FieldInfo f in fieldInfos)
{
System.Console.WriteLine("Filed: {0}", f.Name);
}
foreach(MethodInfo m in methodInfos)
{
System.Console.WriteLine("Method: {0}", m.Name);
}
}
}
输出结果如下:
GetType方法也会调用typeof运算符,该方法对每个对象的每个类型都有效。区别如下:
- Typeof的参数只能是int,string,String,自定义类型,且不能是实例
- GetType()和typeof都返回System.Type的引用.
- TypeOf():得到一个Class的Type
- GetType():得到一个Class的实例的Type
第十二章
1.数组类型
C#有两种数组类型,一种是矩形数组,一种是交错数组。
1.矩形数组
- 某个维度的所有子数组有相同长度的多位数组
- 不管有多少维度,只有一组方括号
2.交错数组
- 每个子数组都是独立数组的多维数组
- 可以有不同长度的字数组
- 数组有多少维度就使用多少组方括号
一图胜千言:
2.数组对象
在C#中数组是一种从System.Array继承来的对象,因此也继承了一些有用的方法
- Rank 返回数组的维度属性
- Length 返回数组的长度的属性
这里要特别说明一下交错数组。交错数组的声明如下:
int[][] SomeArray;
实例化:
int [][] jagArray=new int[3][];
注意:不能在声明语句中初始化顶层数组之外的数组,下面就是错误的:
int[][] jagArray=new int[3][4];
不同于其它数组,交错数组的初始化不能在一个步骤中完成,由于交错数组是独立数组的数组,即每一个数组必须独立创建,实例化整个数组需要分两步走:
- 首先,实例化顶层数组
- 其次,分别实例化每个子数组,把新建数组的引用赋值给他们所属数组的每一个元素
int [][] Arr=new int[3][];
Arr[0]=new int[] {1,2,3};
Arr[1]=new int[] {22,3,233,12};
Arr[2]=new int[] { 34,7,8,99,789,3,1};
交错数组还可以和矩阵数组结合,构成更复杂的数组,下面给一个具体的代码实例。
int[][,] Arr;
Arr = new int[3][,];
Arr[0] = new int[,] {
{10,20},
{100,200}
};
Arr[1] = new int[,]{
{3,4,5},
{12,13,14}
};
Arr[2] = new int[,]{
{4,5,6,8},
{32,42,67,8}
};
for (int i = 0; i < Arr.GetLength(0);i++)//获取Arr维度0的长度
{
for (int j = 0;j<Arr[i].GetLength(0);j++)
{
for (int k = 0; k < Arr[i].GetLength(1);k++)
{
System.Console.WriteLine("[{0}][{1},{2}]={3}",i,j,k,Arr[i][j,k]);
}
System.Console.WriteLine("");
}
System.Console.WriteLine("");
}
小节:
在CIL中,一位数组有特定的指令用于性能优化,矩形数组则没有这些指令,并不在相同级别进行优化,因此,使用一维数组的交错数组比矩形数组更有效率。但另一方面,矩形数组的编程复杂度要小得多。
3.数组拷贝
数组有一个clone方法,clone方法为数组进行浅复制,也就是说,它只创建数组本身的克隆,如果是引用类型数组,它不会复制元素引用的对象。对于值类型和引用类型数组,会有不同的结果。只需要记住以下几点:
- 克隆值类型数组会产生两个独立的数组
- 克隆引用类型数组会产生指向相同对象的两个数组
- clone方法返回object类型的引用,它必须强制转化为数组类型
示例如下:
int[] Intarray1={1,2,3};
int[] Intarray2=(int[])Intarray1.Clone();
这个过程的执行步骤如下:
而对于引用类型数组,同样先给出一个例子:
class A
{
public int value=5;
}
class Program
{
static void main()
{
A[] Arr1=new A[3]{new A(),new A(),new A()};
A[] Arr2=(A[]) Arr1.Clone();
Arr2[0].value=100;
Arr2[1].value=200;
Arr2[2].value=300;
}
}
其执行过程如下:
可以看出,引用类型数组复制时,并没有生产两个独立的数组,它们的每个成员共用同一个对象。
第十三章
1.委托
学过C++的都知道C++里面有一种指向函数的指针叫做函数指针,由于C#中没有指针的概念,所以就没有办法将函数当作参数一样传递出去,不过C#给出了更好的解决办法,那就是委托。你可以将委托理解为C++中一个类型安全面向对象的函数指针,但委托似乎更强大。委托的使用方法与类有些类似:
可以吧delegate看作一个包含有序方法列表的对象,这些方法具有相同的签名和返回类型,方法的列表称为调用列表,委托所保存的方法可以来自任何类或结构,只要它们匹配以下两点:
- 委托的返回类型
- 委托的签名(包括ref和out修饰符)
调用列表中的方法可以是实例方法也可以是静态方法,调用委托时,会执行其列表中的所有方法。
废话不多说,先上实例:
using System;
delegate void MyDel(int value);//声明委托类型
namespace test_1
{
public class test_delegate
{
void PrintLow(int value)
{
System.Console.WriteLine("{0}- Low Value", value);
}
void PrintHight(int value)
{
System.Console.WriteLine("{0}- High Value", value);
}
static void Main()
{
test_delegate test = new test_delegate();
MyDel del; //声明委托
//创建随机整数生成器对象,并得到0到99之间的一个随机数
Random random = new Random();
int randValue = random.Next(99);
del = randValue < 50 ? new MyDel(test.PrintLow) : new MyDel(test.PrintHight);
del(randValue);
}
}
}
在上述代码中,创建委托的方法也可以等价于:
del=randValue<50? test.PrintLow: test.PrintHigh;
也就是说,del=new Mydel(obj.func_1) 可以简写为 del=obj.func_1 .这样可行的原因是因为方法名称和其相应的委托类型之间存在隐式转换。
委托可以使用额外的运算符来组合,这个运算符会创建一个新的委托,示例如下:
Mydel delA=obj.func_1;
Mydel delB=obj.func_2;
Model delC=delA+delB;
委托是恒定的,委托对象被创建后不能再改变。
尽管委托不变,但是C#为委托提供了添加和删除的运算符:+=,-=。
Mydel del=obj1.f1;
del+=obj2.f1;
del+=obj3.f3;
-=运算符的使用方法类似。对于上述委托,使用参数调用委托时,就会使用相同参数调用它的调用列表中的每一个成员。
即: del(55)的运行过程如下:
那么问题来了,如果委托方法是有返回值的,那么当一个委托对象的调用列表包含多个方法时,调用的最后一个方法的返回值就是委托调用的返回值,而前面的方法的返回值会被忽略。如果委托方法中有引用参数,那么在调用过程中参数会发生改变。
2.Lambda表达式
C#在2.0的时候引入了