前不久公司组织了C#学习小组,期间我做了两次分享。现把内容整理下分享给大家。 这次分享的内容主要是一些基础性知识,如方法,枚举,结构体,数组,值类型和引用类型的区别与联系,可空类型等。 虽然基础但是再次系统地学习下也是蛮好的,温故而知新嘛。
1 方法和参数修饰符
1.1 参数修饰符
在C#的世界中,常用的参数修饰符有三个 out
, ref
, params
。
用途 | 备注 | |
---|---|---|
out | 将数据从函数体中以参数的形式传递出来 | 标记为out的参数,在函数体中必须为其赋值 |
ref | 使得函数体内代码可以修改函数作用域外的变量 | 与out相比,赋值不是必须的 |
params | 函数可以接受任意个数的参数 | 参数必须是相同类型,并且标记为params类型的参数,必须是最后一个参数 |
例子:
- out
两数相加,并以参数形式将结果返回:
void Sum(int a, int b, out int c) |
- ref
交换两个整数的值:
void Swap(ref int a, ref int b) |
注意: 交换两个整数数值有多种算法,这里利用了 \(a \oplus a = 0\) 的特性。
- params
计算任意数量整数的和:
int Sum(params int[] values) |
1.2 可选参数
可选参数与C++中的默认参数类似,但与之不同的是,C#无严格的定义和声明之分。C++是在声明的时候指定默认参数值,而C#是在定义方法的时候。 顾名思义,可选参数是可有可无的,也就是说在调用的时候可以传递也可以不传递。
可选参数的语法如下:
int Add(int a=5, int b=5) |
调用的时候,我们可以这样调用:
// 不传递参数与Add(5,5)等效 |
需要注意的是,可选参数只能出现在参数列表的末尾部分,以下可选参数的定义是违法的:
// 非法 |
1.3 命名参数
命名参数是.NET Framework 4.0中引入的新语法,使用命名参数语法可以在调用的时候指定参数前缀,改变参数顺序,让程序的可读性更强。例如:
// 求两数之和 |
虽然命名参数支持调用时改变参数顺序,但是为了保持与定义的一致性,建议不这么做。
1.4 方法重载
C语言中是没有重载这个概念的,不允许出现相同名字的函数。C++引入了重载的概念,支持多个函数使用相同的函数名,但是必须满足一定的规则。 有兴趣的可以看下 函数名称重整 。C#和Java也支持方法重载。C#中函数的全名称称之为 方法签名 : 通过指定方法的访问级别(例如 public 或private)、可选修饰符(例如abstract 或sealed)、返回值、名称和任何方法参数,可以在类或 结构中声明方法。 这些部分统称为方法的“签名”。重载的规则包括:
- 不能通过返回类型重载
例如,
public class Calculator |
这是非法的重载
- 参数类型不同可以实现重载
例如,
public class Calculator |
这是允许的
- 参数顺序不同可以实现重载
例如,
public class Calculator |
这也是允许的。
- 参数个数不同可以实现重载
例如,
public class Calculator |
虽然很少有人这样写,但是这种写法确实可以实现重载。
还有其他一些规则,不过这四条规则是最常用的。另外,泛型的出现在一定程度上弱化了重载的必要性。
2 数组
数组是一种常用的数据结构。定义一个数组,需要指定两个要素:1. 数组元素类型; 2. 维度。
2.1 初始化数组
- 显示初始化
int[] numbers = new int[]{1,2}; |
显式指明数组元素类型为整型类型,维度为1,长度为2。
- 隐式初始化
var numbers = new[] { 1, 10, 100, 1000 }; |
隐式初始化利用了C#的var关键字,编译器会对其类型自动推断。使用这种语法初始化数组,数组的元素类型必须是相同的,否则编译器会报错。
2.2 多维数组
与一维数组一样,定义多维数组时也需要指定元素类型和维度,例如,我们可以用下面的代码定义一个二维数组:
int [,] numbers = new int[2,2]{ {1, 3}, {2, 4} }; |
数组这一部分还涉及到C#数组内置的一些方法的使用,这里不进一步介绍了。
3 枚举
好多编程语言中都有枚举这个概念。在某些语言中,有些情况下可以使用常量和宏来实现类似效果。但是枚举的好处多多:
- 枚举是一种强类型,写代码时编译器可以给予语法检查,尽早地排除错误;
- 可以自定义底层存储空间, 使用比较灵活,优化内存;
- 值域是固定的,可枚举的;
3.1 自定义枚举类型
以下代码定义了一个性别枚举:Female和Male。
public enum Gender |
默认的,Female的值为0, Male的值为1。底层的存储类型默认为整型。
当然我们,可以控制枚举值的存储:
public enum Gender:byte |
现在,Gender的枚举成员是byte类型,Female的值为1,Male的值为2.
3.2 声明和使用枚举变量
以下代码定义了一个Gender类型的变量,其值为Gender.Female.
Gender g = Gender.Female; |
3.3 打印枚举的Name/Value对
以下代码可以打印枚举的详细信息。
// This method will print out the details of any enum. |
Enum.GetUnderlyingType
获取底层的存储类型;Enum.GetValues(e.GetType())
获取枚举类型的所有Name/Value对。
3.4 枚举的底层实现
从下图中可以看出,自定义枚举类型继承于 System.ValueType
.
Figure 1: 系统数据类型类图
使用ildasm.exe来检查我们刚才定义的Gender类型的元数据:
.class public auto ansi sealed ForegroundAndBackgroudThreadDemo.Gender extends [mscorlib]System.Enum { } // end of class ForegroundAndBackgroudThreadDemo.Gender
可见,自定义的Gender类型直接继承于 System.Enum
类型。
4 结构体类型
4.1 自定义结构体
结构体的定义非常简单,类似于定义类:
public struct Point |
4.2 创建结构体变量
结构体也是一种类型,变量的声明遵循基本的C#语法:
Point p; |
4.3 结构体与类的区别和联系
结构体类有些类似,但差异还是比较明显的:
结构体 | 类 | 备注 | |
---|---|---|---|
存储 | 栈上 | 堆上 | |
继承 | 不支持 | 支持 | |
自定义构造函数 | 支持 | 支持 | |
适用场景 | 数学,几何,原子类型的实体 | 其他 | |
效率 | 高 | 低 | 因为结构体在栈上创建不涉及垃圾回收 |
如果对效率要求比较高考虑使用结构体,如果合适的话。
4.4 结构体的底层实现
通过ildasm.exe来查看刚才我们定义的结构体的元数据:
.class public sequential ansi sealed beforefieldinit ForegroundAndBackgroudThreadDemo.Point extends [mscorlib]System.ValueType { } // end of class ForegroundAndBackgroudThreadDemo.Point
与枚举不同的是,结构体直接继承于 System.ValueType
,而不是 System.Struct
.
5 值类型和引用类型
参考前面提到C#类组织结构图1。值类型和引用类型是.NET中的两种基本数据类型。 我们平时用到的类型,要么直接要么间接继承于它们。它们之间最大的不同是存储空间的不同,值类型存储在栈上,引用类型存储在堆上。 因此,他们的生命周期也不同:引用类型依赖于垃圾回收机制而值类型跟作用域有关。
值类型一般是存储在栈上的,但是如果放入到一些集合中,会涉及到boxing和unboxing的操作。那时他们的值会被复制到堆上存储。
5.1 变量赋值
- 值类型
复制变量的值,例如,
int a=10; |
代码运行过后, a
的值为2, b
的值为10.
a | b | |
---|---|---|
int a=10 | 10 | N/A |
int b=a | 10 | 10 |
a=2 | 2 | 10 |
b=10 | 2 | 3 |
- 引用类型
复制引用值,有点类似于C++中的指针。以一个例子说明,
public class MyInt |
我们这里定义了一个自定义类型, MyInt
. 赋值语句与值类型类似,
MyInt a = new MyInt{Value=10}; |
代码运行后, a.Value
和 b.Value
都为3。
a.Value | b.Value | |
---|---|---|
MyInt a = new MyInt{Value=10} | 10 | N/A |
MyInt b = a; | 10 | 10 |
a.Value = 2 | 2 | 2 |
b.Value = 3 | 3 | 3 |
5.2 参数传递
前面提到了关键字 ref
.这个关键字适用于值类型和引用类型,使用该关键字时,参数按引用传递,否则默认按值传递。
因此,有四种情况需要讨论:以值传递值类型;以引用传递值类型;以值传递引用类型;以引用传递引用类型。
这里均已类似的加法运算来说明参数的传递机制。
- 以值传递值类型
对于值类型,如果传递从参数时不添加 ref
修饰符,则以值方式传递参数。
Figure 2: 以值传递值类型
在调用Add方法时,本地变量 a
和 b
会被创建在Add方法的栈帧中,与栈帧Main中的 a
和 b
毫无关系,二者是独立的。
在Add方法中改变 a
和 b
的值不会对Main函数中的变量造成任何影响。
- 以引用方式传递值类型
添加了 ref
的参数,调用时会以引用的方式传递参数:
Figure 3: 以引用方式传递值类型
Add方法被调用后,变量 a
和 b
会被创建在其栈帧中。但是他们会指向Main中的 a
, b
变量空间。
因此,在Add方法中改变 a
和 b
的值,Main中的值也会改变。
- 以值的方式传递引用类型
这里还是以一个简单的加法运算来讲解,但是需要另外新建一个 MyInt
类,如图:
Figure 4: 以值方式传递引用类型
引用类型的变量其值最终会在堆上创建。以值的方式传递,传递的只是对那块内存空间的引用值。
所以,在这种方式下,Main和Add中的 a
和 b
都可以修改堆上的存储空间的值。
- 以引用的方式传递引用类型
这种方式是最复杂的,但是现实中用到的场景却不多,一般按值传递就够了。
Figure 5: 以引用方式传递引用类型
Add方法中的 a
和 b
指向的是Main中的对应的存储空间,因此Add方法中的变量同时有改变Main中变量的引用值和堆上的值得能力。
6 可空类型
可空类型的诞生与数据库有关,因为数据库中所有类型的字段都是可以标记为空的。 但是,C#中值类型的数据是不能赋值为null的,只有引用类型才可以。可以说可空类型只是数值类型的一个 Wrapper. 下面我们定义一个可空类型的整型变量并赋值。
int? a=10; |
可空类型的变量与普通值类型的变量的取值有些不同:不能直接使用,需要通过一些特定的方法来取值。
// 第一种取值方式,判断a是否有值,如果有值则直接打印反之直接打印0 |
基本上有两种方式:第一种使用 HasValue
属性来判断是否有值,如果有值则使用 Value
属性直接取值;
第二种方式借助于C#的语法糖 ??
,这只是一种简写方式,与第一方式效果一致。
7 总结
本文简单介绍了下C#基础知识,如方法,枚举,结构体,数组,值类型和引用类型的区别与联系,可空类型等。 虽然基础,但是再次系统学习下也是挺好的。有些地方可能描述得不太准确,请读者见谅。