C#编程基础2

前不久公司组织了C#学习小组,期间我做了两次分享。现把内容整理下分享给大家。 这次分享的内容主要是一些基础性知识,如方法,枚举,结构体,数组,值类型和引用类型的区别与联系,可空类型等。 虽然基础但是再次系统地学习下也是蛮好的,温故而知新嘛。

1 方法和参数修饰符

1.1 参数修饰符

在C#的世界中,常用的参数修饰符有三个 out, ref, params

Table 1: 参数修饰符
  用途 备注
out 将数据从函数体中以参数的形式传递出来 标记为out的参数,在函数体中必须为其赋值
ref 使得函数体内代码可以修改函数作用域外的变量 与out相比,赋值不是必须的
params 函数可以接受任意个数的参数 参数必须是相同类型,并且标记为params类型的参数,必须是最后一个参数

例子:

  • out

两数相加,并以参数形式将结果返回:

void Sum(int a, int b, out int c)
{

c=a+b;
}
  • ref

交换两个整数的值:

void Swap(ref int a, ref int b)
{

int c=a^b;
a^=c;
b^=c;
}

注意: 交换两个整数数值有多种算法,这里利用了 \(a \oplus a = 0\) 的特性。

  • params

计算任意数量整数的和:

int Sum(params int[] values)
{

return values.Sum(e=>e);
}

1.2 可选参数

可选参数与C++中的默认参数类似,但与之不同的是,C#无严格的定义和声明之分。C++是在声明的时候指定默认参数值,而C#是在定义方法的时候。 顾名思义,可选参数是可有可无的,也就是说在调用的时候可以传递也可以不传递。

可选参数的语法如下:

int Add(int a=5, int b=5)
{

return a+b;
}

调用的时候,我们可以这样调用:

// 不传递参数与Add(5,5)等效
Add();

// 只传递一个参数与Add(x,5);等效
Add(10);

// 传递两个参数
Add(10,20); // 计算10+20的和

需要注意的是,可选参数只能出现在参数列表的末尾部分,以下可选参数的定义是违法的:

// 非法
int Add(int a=10, int b)
{

return a+b;
}

// 合法
int Add(int a, int b=0)
{

// ...
}

1.3 命名参数

命名参数是.NET Framework 4.0中引入的新语法,使用命名参数语法可以在调用的时候指定参数前缀,改变参数顺序,让程序的可读性更强。例如:

// 求两数之和
int Add(int a, int b){
// ...
}

// 调用时,可以这样调用
int Add(a:10, b:20);

// 也可以这样调用
int Add(b:20, a:10);

虽然命名参数支持调用时改变参数顺序,但是为了保持与定义的一致性,建议不这么做。

1.4 方法重载

C语言中是没有重载这个概念的,不允许出现相同名字的函数。C++引入了重载的概念,支持多个函数使用相同的函数名,但是必须满足一定的规则。 有兴趣的可以看下 函数名称重整 。C#和Java也支持方法重载。C#中函数的全名称称之为 方法签名 : 通过指定方法的访问级别(例如 public 或private)、可选修饰符(例如abstract 或sealed)、返回值、名称和任何方法参数,可以在类或 结构中声明方法。 这些部分统称为方法的“签名”。重载的规则包括:

  • 不能通过返回类型重载

例如,

public class Calculator
{
public int Add(int a, int b)
{

return a+b;
}
public double Add(int a, int b)
{

return a+b;
}
}

这是非法的重载

  • 参数类型不同可以实现重载

例如,

public class Calculator
{
public int Add(int a, int b)
{

// ...
}

public double Add(double a, double b)
{

// ...
}
}

这是允许的

  • 参数顺序不同可以实现重载

例如,

public class Calculator
{
public double Add(double a, string b)
{

// ...
}

public double Add(string a, string b)
{

// ...
}
}

这也是允许的。

  • 参数个数不同可以实现重载

例如,

public class Calculator
{
public int Add(int a, int b)
{

// ...
}

public int Add(int a, int b, int c)
{

// ...
}
}

虽然很少有人这样写,但是这种写法确实可以实现重载。

还有其他一些规则,不过这四条规则是最常用的。另外,泛型的出现在一定程度上弱化了重载的必要性。

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 枚举

好多编程语言中都有枚举这个概念。在某些语言中,有些情况下可以使用常量和宏来实现类似效果。但是枚举的好处多多:

  1. 枚举是一种强类型,写代码时编译器可以给予语法检查,尽早地排除错误;
  2. 可以自定义底层存储空间, 使用比较灵活,优化内存;
  3. 值域是固定的,可枚举的;

3.1 自定义枚举类型

以下代码定义了一个性别枚举:Female和Male。

public enum Gender
{
Female,
Male
}

默认的,Female的值为0, Male的值为1。底层的存储类型默认为整型。

当然我们,可以控制枚举值的存储:

public enum Gender:byte
{
Female=1,
Male=2
}

现在,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.
static void EvaluateEnum(System.Enum e)
{

Console.WriteLine("=> Information about {0}", e.GetType().Name);
Console.WriteLine("Underlying storage type: {0}",
Enum.GetUnderlyingType(e.GetType()));
// Get all name/value pairs for incoming parameter.
Array enumData = Enum.GetValues(e.GetType());
Console.WriteLine("This enum has {0} members.", enumData.Length);
// Now show the string name and associated value, using the D format
// flag (see Chapter 3).
for(int i = 0; i < enumData.Length; i++)
{
Console.WriteLine("Name: {0}, Value: {0:D}",
enumData.GetValue(i));
}
Console.WriteLine();
}
  • 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
{
public int X;
public int Y;
}

4.2 创建结构体变量

结构体也是一种类型,变量的声明遵循基本的C#语法:

Point p;

4.3 结构体与类的区别和联系

结构体类有些类似,但差异还是比较明显的:

Table 2: 结构体与类的区别与联系
  结构体 备注
存储 栈上 堆上  
继承 不支持 支持  
自定义构造函数 支持 支持  
适用场景 数学,几何,原子类型的实体 其他  
效率 因为结构体在栈上创建不涉及垃圾回收

如果对效率要求比较高考虑使用结构体,如果合适的话。

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;
int b=a;

a=2;
b=3;

代码运行过后, a 的值为2, b 的值为10.

Table 3: 值类型变量赋值举例
  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
{
public int Value;
}

我们这里定义了一个自定义类型, MyInt . 赋值语句与值类型类似,

MyInt a = new MyInt{Value=10};
MyInt b = a;

a.Value = 2;
b.Value = 3;

代码运行后, a.Valueb.Value 都为3。

Table 4: 引用类型赋值举例
  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方法时,本地变量 ab 会被创建在Add方法的栈帧中,与栈帧Main中的 ab 毫无关系,二者是独立的。 在Add方法中改变 ab 的值不会对Main函数中的变量造成任何影响。

  • 以引用方式传递值类型

添加了 ref 的参数,调用时会以引用的方式传递参数:

Figure 3: 以引用方式传递值类型

Add方法被调用后,变量 ab 会被创建在其栈帧中。但是他们会指向Main中的 a, b 变量空间。 因此,在Add方法中改变 ab 的值,Main中的值也会改变。

  • 以值的方式传递引用类型

这里还是以一个简单的加法运算来讲解,但是需要另外新建一个 MyInt 类,如图:

Figure 4: 以值方式传递引用类型

引用类型的变量其值最终会在堆上创建。以值的方式传递,传递的只是对那块内存空间的引用值。 所以,在这种方式下,Main和Add中的 ab 都可以修改堆上的存储空间的值。

  • 以引用的方式传递引用类型

这种方式是最复杂的,但是现实中用到的场景却不多,一般按值传递就够了。

Figure 5: 以引用方式传递引用类型

Add方法中的 ab 指向的是Main中的对应的存储空间,因此Add方法中的变量同时有改变Main中变量的引用值和堆上的值得能力。

6 可空类型

可空类型的诞生与数据库有关,因为数据库中所有类型的字段都是可以标记为空的。 但是,C#中值类型的数据是不能赋值为null的,只有引用类型才可以。可以说可空类型只是数值类型的一个 Wrapper. 下面我们定义一个可空类型的整型变量并赋值。

int? a=10;
int? b=null;

可空类型的变量与普通值类型的变量的取值有些不同:不能直接使用,需要通过一些特定的方法来取值。

// 第一种取值方式,判断a是否有值,如果有值则直接打印反之直接打印0
if (a.HasValue)
{
Console.WriteLine("Value of a is: {0}", a.Value);
}
else
{
Console.WriteLine("Value of a is: {0}", 0);
}

// 另一种取值方式,这只是一种语法糖,效果与第一种方式相同
Console.WriteLine("Value of b is: {0}", b ?? 0);

基本上有两种方式:第一种使用 HasValue 属性来判断是否有值,如果有值则使用 Value 属性直接取值; 第二种方式借助于C#的语法糖 ?? ,这只是一种简写方式,与第一方式效果一致。

7 总结

本文简单介绍了下C#基础知识,如方法,枚举,结构体,数组,值类型和引用类型的区别与联系,可空类型等。 虽然基础,但是再次系统学习下也是挺好的。有些地方可能描述得不太准确,请读者见谅。