这里是跟着微软官方的C#文档学习C#笔记。着重说明了C#和C++之间的区别。
C#是如何工作的
C#和Java十分类似,是一种静态的,带有GC的编译型语言。C#和Java一样有个“虚拟机”,即.Net平台。C#将代码编译成IL(平台无关代码),然后让.Net去执行。
类
C#中所有的类型都是从object
中派生而来。
class和struct
class就是普通的class,struct则是不能被继承,同时也不能继承别人(其从System.ValueType
隐式派生)。struct的出现主要是为了存储数据。
struct和class的主要区别如下:
- 不能继承(但是可以继承接口)
- 不能被继承
- 不能含有abstract,protected或virtual成员
- 可以不使用new操作符进行实例化,这时,你必须手动赋值所有成员之后才能使用该结构体
- 可以有有参构造函数(无参的是默认定义且不可以被改变的),但不能有析构函数
- 不能在成员变量定义时初始化
- 类在堆上分配,结构在栈上分配
简单的class声明:
|
|
注意这里子类中虽然使用了new
代表是重写方法,但是并不能在继承链中起作用。要起作用还是得变成虚函数:
|
|
需要注意:
- 想要进行多态的重写必须加上override,不然默认会加上new关键字(即重写,但不是多态)。
- 不使用virtual的函数不能被override
new关键字也可以放在子类成员变量中以表示隐藏父类同名变量(默认是new)
纯虚函数使用abstract
(即抽象函数):
|
|
sealed
关键字如同C++中的final
,用在方法和类前面用于停止继承。
从派生类访问基类成员要使用base
关键字。
接口interface
和Java一样,你可以将Interface视为C++中的纯虚类:
|
|
如果继承了多个接口,可以使用接口类型进行限定实现:
|
|
枚举
和C++一样,使用enum
关键字:
|
|
C#中的enum都是C++中的enum class
,需要通过枚举名访问,不能隐式转换为整型。
函数(方法)
C#和Java一样,所有的东西都是对象,所以理论上只有类方法。
在C#8之后可以使用面向过程式的写法,类似于Python一样不需要类直接编写代码:
|
|
引用传递参数
使用ref声明的参数是引用参数,使用引用方式传递。
C#和Java一样,基本数据类型默认按值传递,其他类型按引用传递。而且C#还可以通过装箱对基本数据类型进行引用传递:
|
|
所以ref的作用可能只是让基本数据类型进行引用传递吧。
in和out参数
所有参数默认是in参数。
out参数指明此参数可以在不进行初始化的情况下按引用方式传递。并且当调用函数时,需要显式指出是out参数:
|
|
数组参数/不定参数
说是数组参数,其实表现得更像是不定参数。
即将数组当做参数(但数组的长度不是固定的),有如下规定:
- 必须使用
params
指定数组参数 - 必须放参数列表最后
|
|
紧凑表示法和Lambda
|
|
这有点像Lambda:
|
|
一些琐碎知识点
类型隐式转换
- 不同类型之间不能转换(如枚举到整型/整型到枚举)
- 相似类型之间不能进行窄缩转换(如long不能到int,double不能到float,float不能到int)
注意:由于以上的规则,int/float是不能转换到bool的,这意味着
|
|
是无法通过编译的
var
作用同C++11的auto
可为null的类型
和Swift一样,使用type?
来表示此变量可以接受null:
|
|
强制初始化
变量在使用前必须初始化/赋值,不然会报错。
语句
拥有和C++一样的if,for,switch,while语句。只有foreach不一样:
|
|
相等性比较
对于值类型,使用==
即可。
对于引用类型,要判断引用的是否为同一变量,使用Syste.Object.ReferenceEquals(a, b)
。
对于浮点数,可以使用Float.Epsilon
和Double.Epsilon
辅助判断。
条件编译和#define系列语句
没错C#是有这些东西的,而且和C++的用法一模一样。
访问修饰符
public
,private
,protected
: 同C++internal
:仅可访问当前程序集protected internal
:仅可访问此类,此类的派生类和同一程序集中的类private internal
:此类或同一程序集中的类
程序集
在一个大项目中可能包含多个小项目,这些小项目就是程序集。
所以程序集可以是可执行文件或者链接库。
常用数据结构
使用数据结构前(除了数组)必须包含System.Collections
数组
数组和声明和Java如出一辙:
|
|
元素一定会被初始化。基本数据类型初始化为0,bool值为false,可为null的值初始化为null。
类对象会调用默认构造函数。
注意二维数组和交错数组的区别(交错数组是数组的数组,每个数组的长度可以不一样):
|
|
元组
和C++的Tuple类似:
|
|
ArrayList和List
是简单形式的std::vector
,底层是数组,可以自动扩容。缺点是他不是个泛型类,其内部存储的是Object
类型,每次放入/取出元素还要装/拆箱。
而List则是纯纯的std::vector
,其是泛型类。注意List的底层是数组而不是链表,不要和C++弄混了。
HashTable和Dictionary<K,V>
同C++中的std::unordered_map
,底层是哈希表。HashTable
存储的也是Object
类型,需要拆/装箱。而Dictionary是泛型。
HashSet
同C++的std::unordered_set
,底层是哈希表,表示数学意义上的集合。
Queue, Stack
队列和栈
SortedList<K,V>和SortedDictionary<K,V>
自动排序的数组和红黑树(注意SortedDictionary底层是红黑树)
ListDictionary和LinkedList
分别是单链表和双向链表
BitArray
存储位的Array,对二进制位优化了。
HybridDictionary
混合了HashTable
和ListDictionary
的结构,数据量小于8时使用ListDictionary,大于8时将数据移动到HashTable中并使用HashTable管理。
字符串
字符串的关键字是string
,你也可以使用System.String
类型(这两个是一个东西)。
C#的string结尾没有'\0'终止符。
和Java一样,字符串初始化之后是不可变的。所以如下代码只是将新生成的字符串赋值给str1
:
|
|
这也意味着使用str1[0]
方式获得的字符不可被更改。
一些初始化方式
|
|
字符串格式化
使用String.Format
可以格式化:
|
|
空字符串
将String.Empty
赋值以得到空字符串。这比让字符串接收null更好(避免NullException)。
StringBuilder
同Java,StringBuilder创建缓冲区保存所有字符,这意味着可以就地更改字符串内容而不是创建新字符串。
|
|
严格意义上来说StringBuilder更像C++中的std::string
。
使用ToString()
成员方法返回string
。
委托
委托说白了就是观察者模式的一种简化,只不过C#做到语言里面去了。
使用delegate
创建委托,相当于函数指针。
|
|
此委托可以指向任意参数为两个整数,返回值为整数的函数。
声明委托变量:
|
|
可以给委托变量使用+
,+=
增加委托函数,使用-
,-=
去除委托函数。当一个委托中含有多个函数,他就是个多播委托。
Lambda表达式是一种特殊形式的委托。
优雅输出
使用Console.WriteLine()
进行输出。
使用{0}
指定第0个参数,{1}
指定第1个参数:
|
|
使用$""
来允许字符串内插:
|
|
泛型
类似于Java的泛型:
|
|
只需要简单的在函数/类/接口/结构后面增加泛型参数就行了。
也可以对泛型参数进行约束(类似C++20的Concept):
|
|
这里要求T必须是实现了Restrict的类型。
约束存在两种:
- 对继承的约束,比如要求T继承于XX,实现于XX接口等
- 对类型约束,比如T必须为值类型,必须为引用类型,比如不可为null等
对继承的约束好理解,对类型的约束语法如下:
|
|
其中restrict可以为如下值:
struct
:必须为不可为null的值类型class
:必须为不可为null的引用类型class?
:可为null或不可为null的引用类型notnull
:不可为null的类型new()
:必须有公共无参构造函数,且这个约束必须最后指定(而且你没有看错,他确实有个括号)
还有各种其他约束,详见C#微软文档。