C#学习笔记

前言

参考书籍:《C#图解教程》

一、C#编程概述

一个简单的C#程序

1
2
3
4
5
6
7
8
9
10
11
using System;
namespace Simple
{
Class Program
{
static void main()
{
Console.WriteLine("Hello, World!");
}
}
}

文件名为SimpleProgram.cs,这点和Java不太一样,类名和文件名可以不一样。

输入和输出字符串

使用函数WriteWriteLine,其中格式化方法可以使用类似python的方法。例如:

1
2
3
Console.WriteLine("Two sample integers are {0} and {1}",3,6);
// C# 6.0之后可以使用下面的方式
Console.WriteLine($"Two sample integers are {var1} and {var2}");

对数字格式化时可以使用更复杂的表达方式如**{index,alignment:format}**其中index是必选项。

二、类型、储存和变量

栈和堆

以下数据存放在栈中:

  • 某些类型变量的值
  • 程序当前的执行环境
  • 传递给方法的参数

值类型和引用类型

值类型只需要一段单独的内存,用于储存实际的数据,它总是位于堆中。

引用类型需要两段内存,第一段储存实际的数据,它总是位于堆中;第二段是一个引用,指向数据在堆中存放的位置。

三、类

类有两种成员,一种是数据成员,一种是函数成员。

类成员

如果没有初始化,字段的值会被编译器设置为默认值(0和null)。

实例化

实例化一个类的手段和C++类似,都是使用new关键字,同时带上圆括号,需要时可以加上参数。

访问修饰符

  • private只在类的内部可访问
  • public任何类可以访问
  • protected所有继承该类的类可以访问
  • internal该程序集内声明的类可以访问
  • protected internal所有继承该类或者在该程序集内声明的类可以访问

静态成员

可以从类的内部和外部访问这个静态成员,从外部访问时,可以使用using static关键字,从而省去前缀。

静态字段的生命期不需要依赖类,而普通字段只有在实例创建后才开始它的生命期。

静态函数成员也类似,但是静态函数成员不能访问实例成员

常量

常量比较特殊,它们表现得像静态值,但是它们没有真正的存储位置,而是在编译时被编译器替换,类似C/C++的define

属性

属性与字段的相同点和不同点如下:

  • 相同
    • 都是类成员
    • 都有类型
    • 都可以被赋值和读取
  • 不同
    • 属性是一个函数成员
    • 属性不一定为数据存储分配内存
    • 它执行代码

且需要一个set访问器和get访问器

  • set访问器总是拥有一个单独的、隐式的值参,名称为value,与属性的类型相同,拥有一个返回类型void
  • get访问器没有参数,返回一个与属性类型相同的返回类型
1
2
3
4
5
6
7
8
9
10
11
private int RealValue;

public int MyValue
{
set{
RealValue=value;
}
get{
return RealValue;
}
}

属性访问器尝尝使用字段作为后备储存

不能显式地调用set和get访问器,只能使用类似赋值语句的方法。

lambda表达式

在C# 7.0以上,引入的lambda表达式,可以用更简洁的语言来表达访问器。

1
2
3
4
5
int MyValue
{
set=> value>100?100:value;
get=> theRealValue;
}

只有get访问器的属性称为只读属性,只有set访问器的属性称为只写属性,一个属性至少要有两种访问器的其一,否则编译器会报错。与字段相比,属性的好处是可以在读取和输出时完成一些处理。

自动属性

如果想要使用自动属性的话,将get和set的方法体省略,如下

1
2
3
4
public int MyValue
{
set;get;
}

静态属性

静态属性有以下特点:

  • 不能访问类的实例成员,但能被实例成员访问
  • 不管类是否有实例,它都存在
  • 在类的内部,可以仅使用名称来引用静态属性
  • 在类的外部,可以通过类名或者使用using static结构来引用静态属性

构造函数

如果没有为类声明任何构造函数,则编译器会自动定义一个无参数、无函数体的默认构造函数;而如果你已经声明了任何一个构造函数,则这个默认构造函数将不会存在。

静态构造函数

当静态成员被调用时,静态构造函数会随之被调用,静态构造函数只会被调用一次,用于初始化静态成员变量。

readonly修饰符

与const区别如下:

  • const只能在字段的声明语句中初始化,而readonly还可以在类的任何构造函数中使用。
  • const的值必须在编译时确定,而readonly可以在运行时确定。
  • const是静态的,在内存中没有存储位置,而readonly有。

索引器

索引器是一组get和set访问器,与属性类似,属性通常表示单个数据成员,而索引器表示多个数据成员。可以认为索引器是为类的多个数据成员提供set和get访问的属性。

索引器总是实例成员,因此不能被声明为static

声明索引器

1
2
3
4
5
6
7
8
9
10
11
ReturnType this [Type Params1, ...]
{
get
{

}
set
{

}
}

使用this关键字作为名称,参数列表在方括号中,且至少声明一个参数。

索引器可以理解为,用方括号中的参数作为索引,然后在get和set中人为地判断当前索引所指向的成员字段,从而在后续的编程中节省精力。索引器像方法一样,可以被重载,只需要参数类型不同

四、方法

参数

形参:声明在方法的参数列表中

实参:由于初始化形参和表达式或变量

值参数

值类型作为值参数在被传递时,会在栈中开辟一个空间,并将值复制给形参。所以,在方法中无法改变值参数原来的值(除非采用ref关键字),与此相对的是引用类型作为值参数,在方法内修改引用参数的字段时,会相应地修改原实参的值。

引用参数

特点是:

  • 不会在栈上为形参分配内存
  • 形参的参数名作为实参的别名

使用ref关键字来声明一个引用参数。

引用类型作为值参数传递

此时,若从方法体内部修改这个引用类型变量,会直接切断形参与实参之间的联系,可以把引用类型看成一个指针,可以通过指针修改引用类型变量的字段值,但是若想要修改这个引用变量本身,则不会对实参产生任何效果。

引用类型作为引用参数传递

此时可以把形参当做实参的别名使用。

输出参数

使用out关键字来表达一个输出参数,输出参数有以下要求:

  • 在方法内部,给输出参数赋值后才能读取它。
  • 在方法内部,代码中的每条路径都必须为所有输出参数赋值。

在C# 7.0中,可以不需要预先声明一个变量来用作out参数。

1
MyMethod(out MyClass a1, out int a2);

参数数组

关键字为params。

参数数组的重点有:

  • 一个参数列表中只能有一个参数数组
  • 如果有,它必须是最后一个
  • 参数数组表示的所有参数必须是同一类型
1
void ListInts(params int[] inVals);

可以传递由逗号分隔的若干个参数,或一个一维数组来作为实参,当数组在堆中被创建时,实参的值被复制到数组中,所以,可以说参数数组是值参数。

ref局部变量和ref返回

1
ref int y = ref x;

创建一个别名,对x和y变量的修改变得同步。

可以将此技巧和ref返回共同使用。

1
2
3
4
5
6
7
8
9
10
11
{
private int Score;
public ref int RefToVal()
{
return ref Score;
}
}
...
{
ref int OutSide = ref s.RefToVal();
}

这样,就可以获得类成员的引用(即使是private限制)

这个功能有以下限制:

  • ref return表达式不能返回以下内容:
    • 空值
    • 常量
    • 枚举成员
    • 类或者结构体的属性
    • 指向只读位置的指针
  • 只能指向原先就在调用域内的位置或者字段,不能指向方法内的局部变量
  • ref局部变量只能被赋值一次
  • 即使一个方法被声明为ref返回方法,这个方法返回时没有加ref关键字,则这个方法返回的也只是普通类型

命名参数

类似python,但是使用的是冒号而不是等于号

1
c.cal(c:2,b:3,a:5);

这样可以不按声明的顺序来调用方法,且代码可读性也得到了提升,但是位置参数必须在命名参数前面列出。

可选参数

类似python中的默认参数,调用时可以不为其赋值,则采用声明时的默认值。

运算符重载

1
public static MyClass operator +(MyClass a,MyClass b);

该方法必须是静态的和公共的,且运算符必须是要操作的类或者结构的成员。

需要注意,递增和递减运算符的前置和后置操作是有所区别的。

五、类和继承

屏蔽类的成员

可以使用相同的名称来屏蔽父类的成员,若想屏蔽函数成员,则签名必须相同。要让编译器知道你在故意屏蔽父类的成员,可以使用new修饰符,否则编译器可以会发出警告。

访问类的成员

与其他语言不同,C#使用base关键字来表示父类(或者说是基类)

使用基类的引用

可以用类型转化运算将派生类转化为基类,即使两个引用指向的是同样一个实例,但是派生类类型的引用可以“看到”更多成员,而基类类型的引用只能看到在基类中声明的内容(即使被屏蔽)。

虚方法和覆写方法

想要在基类中使用派生类的方法,可以使用虚方法和覆写方法来实现,这个方法有以下要求:

  • 派生类的方法和基类的方法要有相同的签名
  • 基类的方法使用virtual标注
  • 派生类的方法使用override标注

这样,即使派生类的实例被转化成基类,也可以通过这个基类的引用来访问到派生类中的方法。

可以继续让新的类继承这个派生类,并再次覆写某个方法,这样调用基类时,就会优先调用覆写层数最高的方法。

与屏蔽(使用new修饰符)不同,屏蔽的方法只会向上传递一级。

构造函数的执行

构造函数会先执行基类的构造函数。默认情况下,派生类会调用基类的无参数构造函数,但是当你不希望调用无参数构造函数,或基类压根就没有无参数构造函数时,必须在构造函数初始化语句中指定它。

调用父类的构造函数:

1
2
3
4
public MyDerivedClass(int x,string s) : base(s,x)
{
...
}

调用自己的构造函数:

1
2
3
4
public MyDerivedClass(int x) : this(x,"Using default string")
{
...
}

这种手段在存在好几种构造方法时特别好用。

抽象类

抽象类不能被实例化,只能被继承。

抽象成员和虚成员类似,但是抽象成员没有实现体,必须在派生类中被实现;且抽象方法只能在抽象类中存在。

密封类

使用sealed关键字来制定一个密封类,密封类无法被继承。

六、语句和运算符

typeof

typeof返回一个System.Type对象,参数是一个类。

1
2
3
4
5
6
7
class SomeClass
{
...
}
Type t = typeof(SomeClass);
FieldInfo fieldInfo = t.GetFields();
MethodInfo mi = t.GetMethods();

nameof

nameof返回一个字符串,这个字符串是一个变量的非限定名称。

using语句

using语句和using指令不同,using语句是用于打开文件的(类似python的with语句),而using指令是对某个包的使用

1
2
3
4
using (Type resource = new Type(...))
{
...
}

相当于using语句隐式地使用了try…finally…语句。using语句的括号中可以使用多个资源。

七、枚举

1
2
3
4
5
6
enum TrafficLight
{
Green,
Yellow,
Red
}

默认情况下,编译器把int类型的0,1,2…赋值给枚举值,也可以人为地为其赋值来设置自己想要的值。枚举变量可以作为右值赋值给其他变量。

1
2
3
4
5
6
7
8
9
enum FaceCards:uint
{
Jack=11,
Queen,
King,
Ace,
NumberOfFaceCards=4;
SomeOtherValue
}

上面这个程序段显示了如何显示和隐式地给枚举赋值,值得注意的是,枚举中的值可以重复。

位标志

可以为枚举值赋诸如0x01,0x02,0x04,0x08这样的值来使用位标志。

1
2
3
4
5
6
7
8
[Flags]
enum CardCheckSettings : uint
{
SingleDeck = 0x01;
LargePictures = 0x02;
FancyNumbers = 0x04;
Animation = 0x08;
}

八、数组

数组是对象,其表达方式和C语言有所不同

1
2
int[,,] MyArray = new int[3,2,4]{...};
//矩形数组

交错数组

1
int[][] jagArr = new int[3][];

交错数组的声明方式与矩形数组有所不同,可以把它看成是数组的数组,声明之后在为第一个维度的数组赋值。

交错数组可以和foreach语句配合使用

1
2
3
4
5
6
7
foreach(int[] array in arr1)
{
foreach(int item in array)
{
...
}
}

数组的成员

成员 类型 生存期 意义
Rank 属性 实例 数组的维度数
Length 属性 实例 所有维度的元素总数
GetLength 方法 实例 返回指定维度的长度
Clear 方法 静态 将某一范围内的元素设置为0或者null
Sort 方法 静态 对一维数组进行排序
BinarySearch 方法 静态 二分搜索
Clone 方法 实例 浅复制
IndexOf 方法 静态 返回一维数组中遇到的第一个值
Reverse 方法 静态 反转一维数组中某一范围内的元素

C#学习笔记
http://zhouhf.top/2022/07/09/C-学习笔记/
作者
周洪锋
发布于
2022年7月9日
许可协议