rookie-csharp

学习入门csharp的记录

参考: https://www.runoob.com/csharp/csharp-tutorial.html

Tip

  • C#的构想很接近C++,但是它和JAVA更相似,因为我是写C++比较多,所以这个记录会记录我对C#的,从C++角度的一些理解。

  • 如果想通过本文档学习了解C#,需要先熟练编写C++代码,理解面向对象编程思想。

  • 本文档提供简单入门,深入了解需要通过项目来学习,通过本文档是不够的。

环境

简单入门

首先我用的目录结构:

src 里面是每一章节的代码 Lessonx.cs

Main.cs:

然后在Src/Lesson1.cs里定义一些类和接口。

先来打印一个hello world.

程序的第一行 using System; - using 关键字用于在程序中包含 System 命名空间。 一个程序一般有多个 using 语句, 相当于C++的 using namespace std;

后面Test1就是一个函数了,和C++一样,如果不是static方法,需要创建对象才能调用,如果是static方法,可以直接通过类名调用。

就是打印语句,Console是System命名空间的,要包含上才能用。

是针对于VSStudio的,防止命令行快速闪退。

Note

  • C# 是大小写敏感的。

  • 所有的语句和表达式必须以分号(;)结尾。

  • 程序的执行从 Main 方法开始。

  • 与 Java 不同的是,文件名可以不同于类的名称。

编写一个简单的类

只要会一门面向对象语言其实上面的代码都是很好懂的。

Tip

  • 构造函数要带上public不然外面访问不到(细节还没学,后面会详细学)

  • 和C++一样,如果显示编写了构造函数,默认构造就不会自动构造,需要提供无参的构造,不然就不能用new Circle()来构造。

  • 如果不初始化字段,会调用自己的构造函数。上面的int就会默认构造为0。

顶级语句

一句话:可以想像写Python一样写。

特点:

Warning

  • 文件限制:顶级语句只能在一个源文件中使用。如果在一个项目中有多个使用顶级语句的文件,会导致编译错误。

  • 程序入口:如果使用顶级语句,则该文件会隐式地包含 Main 方法,并且该文件将成为程序的入口点。

  • 作用域限制:顶级语句中的代码共享一个全局作用域,这意味着可以在顶级语句中定义的变量和方法可以在整个文件中访问。

类型

值类型

通过程序运行结果就可以得出每一种变量的默认值和变量的空间占的大小。

变量的范围: https://www.runoob.com/csharp/csharp-data-types.html

浅拷贝还是深拷贝

类是浅拷贝。

为了深拷贝,首先需要提供拷贝构造,和cpp一样

然后需要显示调用拷贝构造才能构造新对象。

Caution

Circle c2 = c1; // 就算有拷贝构造,这个也是浅拷贝,不同于C++

引用类型

引用类型不包含存储在变量中的实际数据,但它们包含对变量的引用。

换句话说,它们指的是一个内存位置。使用多个变量时,引用类型可以指向一个内存位置。如果内存位置的数据是由一个变量改变的,其他变量会自动反映这种值的变化。内置的引用类型有:objectdynamicstring

对象(Object)类型

对象(Object)类型 是 C# 通用类型系统(Common Type System - CTS)中所有数据类型的终极基类。Object 是 System.Object 类的别名。所以对象(Object)类型可以被分配任何其他类型(值类型、引用类型、预定义类型或用户自定义类型)的值。但是,在分配值之前,需要先进行类型转换。

当一个值类型转换为对象类型时,则被称为 装箱;另一方面,当一个对象类型转换为值类型时,则被称为 拆箱

动态(Dynamic)类型

您可以存储任何类型的值在动态数据类型变量中。这些变量的类型检查是在运行时发生的。

声明动态类型的语法:

例如:

动态类型与对象类型相似,但是对象类型变量的类型检查是在编译时发生的,而动态类型变量的类型检查是在运行时发生的。

字符串(String)类型

字符串(String)类型 允许您给变量分配任何字符串值。字符串(String)类型是 System.String 类的别名。它是从对象(Object)类型派生的。字符串(String)类型的值可以通过两种形式进行分配:引号和 @引号。

C# string 字符串的前面可以加 @(称作"逐字字符串")将转义字符(\)当作普通字符对待。

用户自定义引用类型有:class、interface 或 delegate。

参考: https://www.runoob.com/csharp/csharp-data-types.html

指针类型

后续说明


下面代码在Lesson2.cs中。

类型转换

隐式和显式

C#中有两种类型转换:隐式和显式,和CPP一样的,很好理解。

显示就是强转,强转可能会导致丢失数据,这个也很好理解,不赘述。

类型转换方法

序号内置方法
1ToBoolean 如果可能的话,把类型转换为布尔型。
2ToByte 把类型转换为字节类型。
3ToChar 如果可能的话,把类型转换为单个 Unicode 字符类型。
4ToDateTime 把类型(整数或字符串类型)转换为 日期-时间 结构。
5ToDecimal 把浮点型或整数类型转换为十进制类型。
6ToDouble 把类型转换为双精度浮点型。
7ToInt16 把类型转换为 16 位整数类型。
8ToInt32 把类型转换为 32 位整数类型。
9ToInt64 把类型转换为 64 位整数类型。
10ToSbyte 把类型转换为有符号字节类型。
11ToSingle 把类型转换为小浮点数类型。
12ToString把类型转换为字符串类型。
13ToType 把类型转换为指定类型。
14ToUInt16 把类型转换为 16 位无符号整数类型。
15ToUInt32 把类型转换为 32 位无符号整数类型。
16ToUInt64 把类型转换为 64 位无符号整数类型。

这些方法都定义在 System.Convert 类中,使用时需要包含 System 命名空间。它们提供了一种安全的方式来执行类型转换,因为它们可以处理 null值,并且会抛出异常,如果转换不可能进行。

参考:https://www.runoob.com/csharp/csharp-type-conversion.html

上面这些都是 Convert 类里面的非静态方法。

当然,System.Convert 也提供了一些静态的方法可以用。

类型转换重载

C# 还允许你定义自定义类型转换操作,通过在类型中定义 implicitexplicit 关键字。

相当于重载,很好理解。

详细的类型转换表格总结:https://www.runoob.com/csharp/csharp-type-conversion.html

stdin 读数据

System 命名空间中的 Console 类提供了一个函数 ReadLine(),用于接收来自用户的输入,并把它存储到一个变量中。

一些其他规则

变量的生命周期

简单变量的生命周期和CPP一样,不赘述。

常量

整数常量可以是十进制、八进制或十六进制的常量。前缀指定基数:0x 或 0X 表示十六进制,0 表示八进制,没有前缀则表示十进制。

整数常量也可以有后缀,可以是 U 和 L 的组合,其中,U 和 L 分别表示 unsigned 和 long。后缀可以是大写或者小写,多个后缀以任意顺序进行组合。

这里有一些整数常量的实例:

以下是各种类型的整数常量的实例:

参考:https://www.runoob.com/csharp/csharp-constants.html

一个浮点常量是由整数部分、小数点、小数部分和指数部分组成。您可以使用小数形式或者指数形式来表示浮点常量。这里有一些浮点常量的实例:

字符常量里面转义字符那些,和其他语言基本相同。

字符串常量:字符串常量是括在双引号 "" 里,或者是括在 @"" 里。字符串常量包含的字符与字符常量相似。

定义常量:

Note

注意:类内常字段用类名访问,而不是用实例化后对象进行访问。

运算符

运算符是一种告诉编译器执行特定的数学或逻辑操作的符号。C# 有丰富的内置运算符,分类如下:

参考:https://www.runoob.com/csharp/csharp-operators.html

前面五种运算符和CPP相同,不赘述。

运算符描述实例
sizeof()返回数据类型的大小sizeof(int),将返回 4.
typeof()返回 class 的类型typeof(StreamReader);
&返回变量的地址&a; 将得到变量的实际地址。
*变量的指针*a; 将指向一个变量。
? :条件表达式如果条件为真 ? 则为 X : 否则为 Y,和CPP的一样,三目
is判断对象是否为某一类型。If( Ford is Car) 检查 Ford 是否是 Car 类的一个对象。
as强制转换,即使转换失败也不会抛出异常。Object obj = new StringReader("Hello"); StringReader r = obj as StringReader;

循环

while, for, do while 和CPP一样。

foreach循环:类似CPP的for(auto e : v)语句。

访问限定符和继承

不写默认是private

protected相关的用于继承,和CPP一样,不赘述。

方法定义(函数定义)

前面已经编写很多函数定义了,不赘述,这里介绍前面没有提到的一些规则。

一个递归调用的例子

用递归求一个阶乘吧

输入参数、输入输出参数、输出参数

这个很好理解,一个优秀的C++工程师在设计三种参数的时候都有讲究,比如:

第一个是输入参数,必须带上const,这是优秀的编码习惯,第二个是输入输出参数,第三种是输出参数,一般用指针来写,这种规范是一定要在编码中体现出来的。

对应C#:

ref表示传引用,out表示纯输出参数。

Note

在 C++ 和 C# 中,参数传递的机制和关键字使用存在一些显著的差异,特别是关于引用和 const 的使用。这些差异主要源于两种语言在设计哲学和运行时行为上的不同。

C++ 中的 const 和引用

在 C++ 中,const 关键字和引用 (&) 被广泛用于参数传递,主要原因是:

  • 性能优化:通过传递引用来避免复制大型对象,例如 std::string,同时使用 const 保证函数不会修改传入的对象。

  • 确保不可变性const 关键字确保函数内部不能修改输入参数,增加代码的安全性和可预测性。

C# 中的参数传递

C# 设计时采用了不同的方法:

  • 引用语义:C# 中的类类型(引用类型)默认就是通过引用传递的,但这是引用的副本,而非对象本身的直接引用。这意味着你可以修改对象的内部状态,但不能修改对象本身的引用。

  • 值类型的传递方式:值类型(如 int, struct 等)默认是通过值传递的,即创建原始数据的副本。如果你需要通过函数修改原始值类型数据,可以使用 refout

  • 不支持 const:C# 没有直接等价于 C++ 中的 const 修饰符。C# 不能声明一个方法的参数为 const,意味着不像在 C++ 中那样在编译时强制禁止修改参数。如果需要保证不修改数据,只能依靠开发者的约定或通过使用不可变对象来实现。

设计哲学差异

  • 简化语言复杂度:C# 设计时尽可能简化语言特性,避免了 C++ 中的一些复杂性,如指针算术和复杂的引用传递规则。

  • 安全性:C# 强调内存和执行安全,自动内存管理(垃圾收集)减少了直接内存操作的需要,也就减少了 const 和指针的必要性。

总结来说,虽然 C# 在设计上不支持像 C++ 那样的 const 修饰符或相同的引用传递语义,但它提供了其它机制(如 readonly 修饰符、不可变集合等)来实现类似的目标。C# 的设计选择更强调简洁性和安全性,虽然这导致了一些灵活性的牺牲。

可空类型 Nullable

简单说明:? 单问号用于对 int、double、bool 等无法直接赋值为 null 的数据类型进行 null 的赋值,意思是这个数据类型是 Nullable 类型的。

?? 双问号用于判断一个变量在为 null 的时候返回一个指定的值。

Tip

int? i = 3; 等同于 Nullable<int> i = new Nullable<int>(3);

?的用法:

??的用法:

简单来说就是,如果为null则返回预设值,否则返回原有的非null值。

数组

常规数组

数组下标从0开始,其他规则基本和C++相同,直接看例子。

多维数组:

交错数组:

本质:数组的数组

可以声明:

声明不会创建空间,需要手动new

另一种初始化的方式:

参考:https://www.runoob.com/csharp/csharp-jagged-arrays.html

交错数组需要两层foreach来访问。

数组作为函数参数

和C++/C相同,数组名为首元素地址(说法不准确,但可以这样理解)

直接看例子就行了

所以是不需要传递数组的大小的。

参数数组(可变参数)

在使用数组作为形参时,C# 提供了 params 关键字,使调用数组为形参的方法时,既可以传递数组实参,也可以传递一组数组元素。

例子:

Note

params参数必须是最后一个参数。

这种操作是合法的。

Array基类

Array 类是 C# 中所有数组的基类,它是在 System 命名空间中定义。Array类提供了各种用于数组的属性和方法。

下表列出了 Array 类中一些最常用的属性:

序号属性 & 描述
1IsFixedSize 获取一个值,该值指示数组是否带有固定大小。
2IsReadOnly 获取一个值,该值指示数组是否只读。
3Length 获取一个 32 位整数,该值表示所有维度的数组中的元素总数。
4LongLength 获取一个 64 位整数,该值表示所有维度的数组中的元素总数。
5Rank 获取数组的秩(维度)。

如需了解 Array 类的完整的属性列表,请参阅微软的 C# 文档。

下表列出了 Array 类中一些最常用的方法:

序号方法 & 描述
1Clear 根据元素的类型,设置数组中某个范围的元素为零、为 false 或者为 null。
2Copy(Array, Array, Int32) 从数组的第一个元素开始复制某个范围的元素到另一个数组的第一个元素位置。长度由一个 32 位整数指定。
3CopyTo(Array, Int32) 从当前的一维数组中复制所有的元素到一个指定的一维数组的指定索引位置。索引由一个 32 位整数指定。
4GetLength 获取一个 32 位整数,该值表示指定维度的数组中的元素总数。
5GetLongLength 获取一个 64 位整数,该值表示指定维度的数组中的元素总数。
6GetLowerBound 获取数组中指定维度的下界。
7GetType 获取当前实例的类型。从对象(Object)继承。
8GetUpperBound 获取数组中指定维度的上界。
9GetValue(Int32) 获取一维数组中指定位置的值。索引由一个 32 位整数指定。
10IndexOf(Array, Object) 搜索指定的对象,返回整个一维数组中第一次出现的索引。
11Reverse(Array) 逆转整个一维数组中元素的顺序。
12SetValue(Object, Int32) 给一维数组中指定位置的元素设置值。索引由一个 32 位整数指定。
13Sort(Array) 使用数组的每个元素的 IComparable 实现来排序整个一维数组中的元素。
14ToString 返回一个表示当前对象的字符串。从对象(Object)继承。

如需了解 Array 类的完整的方法列表,请参阅微软的 C# 文档。

文字来自:https://www.runoob.com/csharp/csharp-array-class.html

这里用了一个 lambda 表达式。

在 C# 中,如果想对数组进行排序并使用自定义的比较方法,可以使用 Array.Sort<T> 方法的几种重载之一,这些重载方法允许你指定比较器。可以通过实现 IComparer<T> 接口或使用 Comparison<T> 委托来定义自定义比较逻辑。

写一个 MyCompare 函数。

上面这个是实现一个Compare函数,其实本质和lambda表达式的是一样的。当然这里我用if, else, 其实C#给我们内置好了,用CompareTo方法就行,和上面是等价的。

Note

注意:实现的 MyCompare 函数是需要 int 返回值的。

这里要注意,思想和C++是一样的。

Array.Sort()都是设置从“小”到“大”排列,这个大/小如何定义是可以设置的

如果MyCompare(x, y)返回的是负整数,表示 x < y;

如果MyCompare(x, y)返回的是0,表示 x = y;

如果MyCompare(x, y)返回的是正整数,表示 x > y;

需要创建一个实现 IComparer<int> 接口的类,然后在该类中实现 Compare类。

Caution

类里的函数名必须为Compare(T x, T y)

这是因为 IComparer<T> 接口规定了一个具体的方法签名,必须遵循这个签名来实现比较逻辑。

另外需要注意:这里要和C++区分开来,C++也可以用类或者函数来当仿函数

String类型

构造

在 C# 中,您可以使用字符数组来表示字符串,但是,更常见的做法是使用 string 关键字来声明一个字符串变量。string 关键字是 System.String 类的别名。

可以使用以下方法之一来创建 string 对象:

输出:

属性

序号属性名称 & 描述
1Chars 在当前 String 对象中获取 Char 对象的指定位置。
2Length 在当前的 String 对象中获取字符数。

常用方法介绍

此部分参考: https://www.runoob.com/csharp/csharp-string.html

比较两个指定的 string 对象,并返回一个表示它们在排列顺序中相对位置的整数。该方法区分大小写。

如果布尔参数为真时,该方法不区分大小写。

连接若干个字符串。

返回一个表示指定 string 对象是否出现在字符串中的值。

创建一个与指定字符串具有相同值的新的 String 对象

从 string 对象的指定位置开始复制指定数量的字符到 Unicode 字符数组中的指定位置。

判断 string 对象的结尾/开头是否匹配指定的字符串。

判断两个字符串是否相同。

把指定字符串中一个或多个格式项替换为指定对象的字符串表示形式。

返回指定字符/字符串第一次出现的索引,索引从 0 开始,也可以指定。

返回char数组中任意字符/字符串第一次出现的索引,索引从 0 开始,也可以指定。

返回一个新的字符串,其中,指定的字符串被插入在当前 string 对象的指定索引位置。

指示指定的字符串是否为 null 或者是否为一个空的字符串。

  1. 连接一个字符串数组中的所有元素,使用指定的分隔符分隔每个元素。

  2. 连接一个字符串数组中的指定位置开始的指定元素,使用指定的分隔符分隔每个元素。

返回指定字符串在当前 string 对象中最后一次出现的索引位置,索引从 0 开始。

移除当前实例中的所有字符,从指定位置开始,一直到最后一个位置为止或指定数量。

替换字符/字符串。

返回一个字符串数组,包含当前的 string 对象中的子字符串,子字符串是使用指定的 Unicode 字符数组中的元素进行分隔的。int 参数指定要返回的子字符串的最大数目。

字符串转字符数组。

大小写转换。

移除当前 String 对象中的所有前导空白字符和后置空白字符。

示例代码如下所示:

Struct 封装

简单使用

直接先放一个使用的例子,和C++的简单使用是相同的。

struct vs class

参考:https://www.runoob.com/csharp/csharp-struct.html

struct的一些特点:

struct vs class :

枚举类型

枚举列表中的每个符号代表一个整数值,一个比它前面的符号大的整数值。默认情况下,第一个枚举符号的值是 0

例如:

基本介绍

前面已经使用到很多类了,这里不赘述上面有的知识点。

Tip

类的默认访问标识符是 internal,成员的默认访问标识符是 private

构造和析构

和C++基本相同,不赘述。

静态成员

无论实例化多少个类,静态成员只有一个副本。

const 字段一样用类名访问。

继承和多态

和C++基本一致,不赘述概念和规则了。

这里放一个函数重写的例子吧。

运算符重载

概念和C++的是相同的,这里放一些例子。

Tip

想让自定义类型支持 Console.WriteLine() 输出,就要重载一个 string ToString() 方法。

这个和C++中的 ostream& operator<<() 是同一个道理。

接口 Interface

参考:https://www.runoob.com/csharp/csharp-interface.html

接口定义了所有类继承接口时应遵循的语法合同。接口定义了语法合同 "是什么" 部分,派生类定义了语法合同 "怎么做" 部分。

接口定义了属性、方法和事件,这些都是接口的成员。接口只包含了成员的声明。成员的定义是派生类的责任。接口提供了派生类应遵循的标准结构。

接口使得实现接口的类或结构在形式上保持一致。

抽象类在某种程度上与接口类似,但是,它们大多只是用在当只有少数方法由基类声明由派生类实现时。

接口本身并不实现任何功能,它只是和声明实现该接口的对象订立一个必须实现哪些行为的契约。

抽象类不能直接实例化,但允许派生出具体的,具有实际功能的类。

这个在这里不展开了,感觉用的比较少,具体可见:https://www.runoob.com/csharp/csharp-interface.html

命名空间

思想和C++是相同的。不展开

预处理

基本和C/C++相同,与 C 和 C++ 不同的是,它们不是用来创建宏。一个预处理器指令必须是该行上的唯一指令。

预处理指令含义
#define定义一个符号,可以用于条件编译。
#undef取消定义一个符号。
#if开始一个条件编译块,如果符号被定义则包含代码块。
#elif如果前面的 #if#elif 条件不满足,且当前条件满足,则包含代码块。
#else如果前面的 #if#elif 条件不满足,则包含代码块。
#endif结束一个条件编译块。
#warning生成编译器警告信息。
#error生成编译器错误信息。
#region标记一段代码区域,可以在IDE中折叠和展开这段代码,便于代码的组织和阅读。
#endregion结束一个代码区域。
#line更改编译器输出中的行号和文件名,可以用于调试或生成工具的代码。
#pragma用于给编译器发送特殊指令,例如禁用或恢复特定的警告。
#nullable控制可空性上下文和注释,允许启用或禁用对可空引用类型的编译器检查。

参考自:https://www.runoob.com/csharp/csharp-preprocessor-directives.html

异常处理

四个关键字。

异常处理机制和C++基本相同,C#中的异常类也是派生出来的,和C++思想是相同的

文件处理

略,这里不赘述了,使用的时候直接看文档即可。

System.Collections容器

这一部分参考:https://gitee.com/chutianshu1981/AwesomeUnityTutorial/

其实就是 C# 版本的STL。

System.Collections 包含非泛型容器类

System.Collections.Generic 包含泛型容器类

.NET Framework 4 起,System.Collections.Concurrent 命名空间中的集合可提供高效的线程安全操作,以便从多个线程访问集合项。 System.Collections.Immutable 命名空间(NuGet 包)中的不可变集合类本质上就是线程安全的,因为操作在原始集合的副本上进行且不能修改原始集合。

很多容器和方法和C++是同样思想的,都是数据结构的封装,使用的区别有但不大。

具体可以见代码 src/Lesson5.cs

C# 版本的八大排序

具体见代码。

于此同时还提供了八大排序的性能测试结果,如图所示。

这个结果也是符合预期的。