CSharp编程语言(更新中)
类库之间的引用依赖
笔记总结 刘铁猛的 C#入门详解
通过Visual Studio创建的每一个项目模板,实际上就是一个引用了特定功能的类库程序集。
在C#中,耦合是指类之间的依赖程度,当一个类库中引用了另一个类库时,它们之间就产生了依赖关系,同样程序的耦合度也会提高。
一、类和对象
1、概念
面向对象顾名思义就是把现实中的事务都抽象成为程序设计中的“对象”,其基本思想是一切皆对象。
- 类是对象的抽象,而对象是类的具体实例。同时在语法上来说,类(
class
)是一种数据类型,将状态(字段)和操作(方法和其他函数成员)组合在一个单元中。 - 类为动态创建的类实例 (
instance
) 提供了定义,实例也称为对象 (object
)。 - 类是抽象的,不占用内存;对象是具体的,占用存储空间。
2、创建对象(实例)
使用 new
关键字创建实例。
Form form = new Form();
3、构造函数和析构函数
构造函数(Constructor
)和析构函数(Destructor
)是面向对象编程中的两个重要概念,它们分别用于在对象创建和销毁的时候执行特定的操作
(1)、构造函数
构造函数是一种特殊的成员方法,它在创建对象时被调用,用于初始化对象的状态。
类可以有多个不同版本(重载)的构造函数,以满足不同的初始化需求。构造函数的名称必须与类名相同,且没有返回类型,包括void
。
构造函数的特点:
- 构造函数与类同名,用于创建类的对象。
- 构造函数可以有多个版本,根据参数的不同进行重载。
- 如果没有显式定义构造函数,编译器会自动生成默认的无参数构造函数。
- 如果显式定义了构造函数,编译器不会再生成默认的无参数构造函数。
class Person
{
public string Name;
public int Age;
// 构造函数
public Person(string name, int age)
{
Name = name;
Age = age;
}
}
static void Main(string[] args)
{
Person person = new Person("小明", 18);
}
(2)、析构函数
析构函数,也被称为终结器(Finalizer
),用于在对象被销毁时执行一些清理操作。与构造函数不同,析构函数在对象销毁时自动被调用,而不是在对象创建时。
Ⅰ、析构函数的特点
- 析构函数与类同名,但在方法名前加上
~
符号。 - 一个类只能有一个析构函数,不能重载。
- 析构函数不能被显式调用,它由垃圾回收器(
GC
)自动调用。
static void Main(string[] args)
{
Person person = new Person("小明", 18);
Console.WriteLine($"{name},今年{age}岁", person.Name, person.Age);
// 对象销毁时,析构函数会自动被调用,执行清理操作
}
class Person
{
public string Name;
public int Age;
// 构造函数
public Person(string name, int age)
{
Name = name;
Age = age;
}
// 析构函数
~Person()
{
// 做一些资源释放和断开连接操作
Console.WriteLine("Connection closed.");
}
}
Ⅱ、析构函数的使用
析构函数用于在对象销毁时执行清理操作,例如释放资源、关闭文件、断开连接等。需要注意的是,C#中的垃圾回收机制会自动管理对象的内存,而不是依赖于析构函数来释放内存。因此,析构函数一般用于释放非托管资源(如文件句柄、数据库连接等),而不是用于释放内存。
4、静态和非静态
(1)、区别
- 静态和非静态的不同之处,就是静态从程序一启动就会一直占用内存,而非静态只有在实例化的时候才会分配内存,每实例化一次对象都会重新分配一次内存。
- 静态类和非静态类的重要区别在于静态类不能实例化。
(2)、静态成员的特性
- 静态成员只被创建一次,所以静态成员只有一份,实例成员有多少个对象,就有多少份。
- 成员需要被共享的时候,方法需要被反复调用的时候,就可以把这些成员定义为静态成员。
- 在静态方法中,不能直接调用实例成员,因为静态方法被调用的时候,对象还有可能不存在。
this/base
关键字在静态方法中不能使用,因为有可能对象还不存在。- 非静态类可以包含静态的方法、字段、属性或事件,它们都隶属于类的成员,不用实例化也可以调用。
- 一个非静态类既有静态成员也有实例成员,无论对一个类创建多少个实例,它的静态成员都只有一个副本;
- 静态方法只能被重载,而不能被重写,因为静态方法不属于类的实例成员;
- 静态类不能够被继承,原因你可以理解为静态类被编译后其实就是抽象类和密封类,也可以这样理解:继承后 实例化对象的时候会先初始化基类构造函数 而基类没有非静态构造函数。
- 静态类不能实现接口。这时你会问我静态类被编译的结果是
abstract
和sealed
类,接口是abstract
类,这并没有影响到方法的实现啊。举个例子我们定义一个静态类,我们知道在一静态类中是不能定义实例化的成员的(比如实例化的方法)。一个类实现一个接口必须要用实例的方法来实现接口中的定义的契约,这个与上面的矛盾。 - 静态类不能被派生,原因是静态类只能从
System.Object
派生,派生继承是OO
的,静态是反OO
的。
(3)、静态构造函数
- 在C#中,静态构造函数是用来初始化静态成员变量或执行静态代码的特殊类型的构造函数。
- 静态构造函数只会在类被加载时执行一次,而且在实例化对象之前执行。
- 静态构造函数没有访问修饰符,也不能带有参数,其名称与类名相同。
public class MyClass
{
public static int MyStaticField;
static MyClass()
{
Console.WriteLine("执行静态构造函数");
MyStaticField = 10;
}
public MyClass()
{
Console.WriteLine("执行构造函数");
}
}
class Program
{
static void Main()
{
// 静态构造函数在类被加载时执行
Console.WriteLine(MyClass.MyStaticField);
// 实例化对象时,实例构造函数被调用
MyClass myObj = new MyClass();
}
}
/*
在上面的示例中,当程序运行时,静态构造函数会先于实例构造函数被调用,输出结果为:
执行静态构造函数
10
执行构造函数
*/
5、类的成员
(1)、字段(field
)
字段是类的成员之一,是一种表示与对象或类型关联的变量。本质意义上,字段就是变量。
Ⅰ、字段分为静态字段和非静态字段
- 非静态字段(实例字段)
依靠类的对象来调用,且每个对象都会有一个独立的实例。
class Program
{
static void Main(string[] args)
{
// 实例字段调用:
// new初始化类,创建类的具体对象
Car baoma = new Car();
baoma.carName = "宝马";
}
}
class Car
{
public string carName;
}
-
静态字段
被
static
关键字修饰的字段,叫做“静态字段”。静态字段不属于任何对象(实例),只属于类。
class Program
{
static void Main(string[] args)
{
// 静态字段调用:类名调用
People.name = "张三";
}
}
class People
{
public static string name;
}
- 字段被初始化的时机
- 实例字段是在对象创建时完成初始化;
- 静态字段是在类型被加载执行时完成初始化。
Ⅱ、readonly
只读字段也分为静态和非静态
- 非静态只读字段
使用readonly
关键字来创建只读的实例字段。
只读实例字段 只能在构造函数中或在声明只读字段时 \textcolor{Red}{只能在构造函数中或在声明只读字段时} 只能在构造函数中或在声明只读字段时对这些字段进行初始赋值。一旦初始化完成,这些字段就不能被修改。
// 使用示例
public class Program
{
static void Main(string[] args)
{
MyClass myObject = new MyClass(10, "Hello");
myObject.PrintValues();
// 以下代码会导致编译错误,因为 _readonlyInt 是只读的
// myObject._readonlyInt = 20;
// 正确的做法是通过方法调用来修改只读字段的值,如果有需要的话
}
}
public class MyClass
{
// 只读实例字段
private readonly int ReadonlyInt;
private readonly string ReadonlyString;
// 构造函数中初始化只读字段
public MyClass(int initValue, string initString)
{
ReadonlyInt = initValue;
ReadonlyString = initString;
}
public void PrintValues()
{
Console.WriteLine(ReadonlyInt);
Console.WriteLine(ReadonlyString);
}
}
ReadonlyInt
和ReadonlyString
字段被声明为readonly
,它们只能在MyClass
的构造函数中被赋值。尝试在Main
方法中更改这些值将导致编译错误。
- 静态只读字段
使用static
和readonly
关键字来创建一个静态只读字段。
静态只读字段是指在类中声明的静态字段,其值只能在 [ 声明静态只读字段时 ] 或 [ 在静态构造函数中 ] 进行赋值。
// 在声明静态只读字段时赋值
public class MyClass
{
public static readonly int MyStaticReadOnlyField = 10; // 静态只读字段
}
// 在静态构造函数中进行赋值
public class MyClass
{
public static readonly int MyStaticReadOnlyField;
static MyClass()
{
MyStaticReadOnlyField = 10;
}
}
字段对内,属性对外。
(2)、属性(property
)
在C#中,属性(Properties
)是一种用于封装类的字段的机制,通过属性可以控制对类的字段的访问。属性通常用于获取(get
)和设置(set
)私有字段的值,同时可以添加验证逻辑、计算逻辑等。
属性的声明
class Example {
private int age; // 字段一般为私有,避免外部直接访问
public int Age
{
// 获取值
get { return age; }
// 设置值
set {
if (value >= 0 && value <= 100)
{
age = value;
} else {
age = 0;
}
}
}
}
Ⅰ、字段与属性的关系
字段(field
)是类中用于存储数据的成员变量,它们直接存储数据的值。字段可以是私有的,也可以是公共的,但直接访问字段可能会导致数据不受控制地被修改,因此通常建议将字段封装在属性中进行访问。
属性(property
)是一种访问器方法,用于控制对类的字段的访问。属性提供了对私有字段的安全访问,可以在获取和设置字段值时执行额外的逻辑,如验证、计算等。属性可以具有get
访问器(用于获取属性值)和set
访问器(用于设置属性值)。
Ⅱ、动态设置属性值(代码示例)
通过获取IsCanWrok
属性值时,验证年龄大于15岁才能工作。
class People {
private int age;
public int Age
{
get {
return age;
}
set {
if (value >= 0 && value <= 100)
{
age = value;
} else {
age = 0;
}
}
}
/// <summary>
/// 年龄大于15岁才能工作
/// </summary>
public bool IsCanWrok
{
get {
if (this.age >= 16)
{
return true;
} else {
return false;
}
}
}
}
通过在设置Age
属性值时,动态的给isCanWrok
字段赋值,然后通过IsCanWrok
属性的get
访问器获取值。
class People {
private int age;
public int Age
{
get {
return age;
}
set {
if (value >= 0 && value <= 100)
{
age = value;
} else {
age = 0;
}
this.GetIsCanWrok();
}
}
private bool isCanWrok;
/// <summary>
/// 年龄大于15岁才能工作
/// </summary>
public bool IsCanWrok
{
get { return isCanWrok; }
}
private void GetIsCanWrok() {
if (this.age >= 16)
{
this.isCanWrok = true;
} else {
this.isCanWrok = false;
}
}
}
(3)、索引器(一般不会使用)
在C#中,索引器(Indexers
)是一种特殊的属性,允许类的实例像数组一样通过索引来访问元素。索引器允许类的实例像数组一样使用方括号[]来访问元素,而不是使用点号.。
索引器通常用于实现集合类,使得可以通过索引来访问集合中的元素。索引器可以具有一个或多个参数,这些参数用于确定要访问的元素。索引器的声明方式类似于属性,但使用this
关键字来定义。
class Student {
private Dictionary<string, double> dicScore = new Dictionary<string, double>(); // 科目分数字典
/// <summary>
/// 学生科目分数索引器
/// </summary>
/// <param name="subject">科目名称</param>
/// <returns></returns>
public double? this[string subject]
{
get {
if (this.dicScore.ContainsKey(subject))
{
return this.dicScore[subject];
} else {
return null;
}
}
set {
// HasValue 属性来检查可空类型是否有值
if (!value.HasValue)
{
value = 0;
}
if (this.dicScore.ContainsKey(subject))
{
dicScore[subject] = value.Value; // 使用可空类型的Value属性来访问可空类型的实际值。
} else {
dicScore.Add(subject, value.Value);
}
}
}
}
static void Main(string[] args)
{
Student people = new Student();
people["数学"] = 100;
Console.WriteLine(people["数学"]); // 返回 100
Console.ReadLine();
}
(4)、常量(Constants
)
常量在C#中使用const
关键字进行声明,通常用于定义程序中不会改变的固定值,例如数学常数、枚举值等。常量必须在声明时进行初始化,并且不能被修改。
常量隶属于类型而不是对象,没有“实例常量”。“实例常量”的角色由只读实例字段来担当。
public static class Math {
public const double PI = 3.1415926535897931;
}
常量(Constants
)和静态只读字段(Static Readonly Fields
)的区别
-
初始化时间:
常量在声明时必须进行初始化,并且只能使用编译时常量表达式来初始化。常量的值在编译时就确定,并且在整个程序中保持不变。
静态只读字段可以在声明时或者在静态构造函数中进行初始化。静态只读字段的值在运行时被确定,并且只能在初始化时或者构造函数中赋值一次。
-
作用域:常量是类级别的,可以在类的任何地方访问,包括类的内部和外部。
静态只读字段也是类级别的,可以在类的任何地方访问,但不能在构造函数中修改其值。
-
性能:常量的值在编译时被替换为实际的值,因此在性能上比静态只读字段更高效。
静态只读字段的值在运行时确定,因此每次访问时都需要进行计算,可能会略微影响性能。
-
类型:常量可以是基本数据类型、字符串、枚举类型等。
静态只读字段可以是任何类型,包括引用类型。
-
总的来说,常量用于存储编译时确定的不可更改值,而静态只读字段用于存储运行时确定的不可更改值。选择使用常量还是静态只读字段取决于值是否在编译时就确定,以及是否需要考虑性能等因素。
6、类的声明和访问级别
在C#中,类的声明和访问级别是通过使用访问修饰符来实现的。
以下是一些常见的访问修饰符及其含义:
(1)、public: 公共访问修饰符
表示类可以从任何地方访问,没有访问限制。
public class MyClass
{
// 类的成员和方法
}
(2)、private: 私有访问修饰符
表示只有在同一类中才能访问该类的成员或方法。
class MyClass
{
private int myPrivateField;
private void MyPrivateMethod()
{
// 方法实现
}
}
(3)、protected: 受保护的访问修饰符
表示只有在同一类或派生类中才能访问该类的成员或方法。
class MyBaseClass
{
protected int myProtectedField;
}
class MyDerivedClass : MyBaseClass
{
public void MyMethod()
{
myProtectedField = 10; // 可以访问基类的受保护字段
}
}
(4)、internal: 内部访问修饰符
表示只有在同一程序集内的类才能访问该类的成员或方法。
internal class MyInternalClass
{
// 类的成员和方法
}
(4)、protected internal: 受保护的内部访问修饰符
表示只有在同一程序集内的类或派生类才能访问该类的成员或方法。
protected internal class MyProtectedInternalClass
{
// 类的成员和方法
}
这些访问修饰符可以用来控制类的成员和方法的访问级别,以确保代码的安全性和封装性。
7、类的特性
(1)、继承
Ⅰ、概念
继承就是派生类继承基类的特征和行为,使得派生类对象(实例)具有基类的属性和方法,或派生类从基类继承方法,使得派生类具有基类相同的行为(派生类能访问基类的属性、方法以及事件)。
本质上是指,派生类在完整接受基类成员的前提下,对基类进行横向(对基类成员进行扩充)及纵向(对基类成员的版本更新或者重写)的扩展。
所有类的最终基类都是Obect
。
static void Main(string[] args)
{
Car car = new Car();
car.Owner = "CN"; // 派生类可以访问基类的成员
}
/// <summary>
/// 交通类
/// </summary>
class Vehicles {
public string Owner { get; set; }
}
/// <summary>
/// 汽车类
/// </summary>
class Car : Vehicles {
}
Ⅱ、特点
- 派生类是对基类的扩展,派生类可以添加新的成员,但不能移除已经继承的成员的定义,保持继承链的完整。
- 构造函数和析构函数不能被继承。
- 派生类只能继承一个基类,但是可以通过接口来实现多重继承。(单基类继承,多接口实现)
- 类的访问级别对继承的影响,派生类也无法继承
private
(私有访问级别,表示该类对自身可见,其他类都不访问) 修饰的基类,也无法访问成员。 sealed
关键字表示为密封类,用于限制类的继承。当一个类被标记为sealed
时,该类不能被其他类继承,即不能有子类。需要注意的是sealed
关键字只能用于类,不能用于接口、结构体或枚举。
Ⅲ、this
和base
this
关键字
this
关键字用于引用当前类的实例。
在类的方法中,可以使用this
来引用当前类的实例变量、属性和方法。
通常用于区分类的成员变量和方法参数之间的同名冲突。
例如,this.variableName
引用当前对象的实例变量,this.MethodName()
调用当前对象的方法。
base
关键字
base
关键字用于引用上一级基类的成员。
在派生类中,可以使用base
来调用基类的构造函数、属性和方法。
通常用于在派生类中重写基类的方法时,调用基类的实现。
例如,base.MethodName()
调用基类的方法,base.PropertyName
引用基类的属性。
/// <summary>
/// 交通类
/// </summary>
class Vehicles {
public string Owner { get; set; }
public Vehicles(string owner)
{
this.Owner = owner;
}
public virtual void Print()
{
Console.WriteLine("BaseClass: " + Owner);
}
}
/// <summary>
/// 汽车类
/// </summary>
class Car : Vehicles {
public Car(string owner) : base(owner)
{
}
// override重写基类方法
public override void Print()
{
base.Print(); // 调用基类的Print方法
Console.WriteLine("DerivedClass: " + owner);
}
}
注:
public Car(string owner) : base(owner)
在自定义有参的构造器时,用于指定Car
派生类的构造函数继承自基类Vehicles
的构造函数,并调用基类的构造函数来完成基类部分的初始化工作。
这是因为在C#中,派生类的构造函数必须调用基类的构造函数,以确保基类的字段和属性得到正确的初始化。在下一节具体说明。
Ⅳ、 构造函数的执行逻辑
当派生类继承基类后,在实例化派生类时,基类的实例也会被创建。
在这种情况下,实例化的派生类会自动调用基类的构造函数来初始化基类的部分。基类的构造函数会在派生类的构造函数中被调用,以确保基类部分也被正确初始化。这意味着在实例化派生类时,基类的实例也会被创建并初始化。
如上,基类Vehicle
和派生类Car
,当实例化Car
类时,会同时创建Car
类和Vehicle
类的实例。派生类的构造函数会调用基类的构造函数来完成基类的初始化工作,确保整个对象的完整性和正确性。
具体代码如下:
static void Main(string[] args)
{
Car car = new Car("小明");
Console.ReadLine();
}
/// <summary>
/// 交通类
/// </summary>
class Vehicles {
public string Owner { get; set; }
public Vehicles(string owner) {
this.Owner = owner;
}
}
/// <summary>
/// 汽车类
/// </summary>
class Car : Vehicles {
public Car(string owner) : base(owner)
{
this.Owner = owner;
}
}
注:
在基类Vehicle
和派生类Car
的构造函数中分别打上断点,观察程序执行的步骤及逻辑。
二、数据类型
1、结构体类型
(1)、值类型
值类型变量存储在栈上,可以直接分配给一个值。它们是从类 System.ValueType
中派生的。而 System.ValueType
类则继承于System.Object
。
类型 | 描述 | 范围 | 默认值 |
---|---|---|---|
bool | 布尔值 | True 或 False | False |
byte | 8 位无符号整数 | 0 到 255 | 0 |
char | 16 位 Unicode 字符 | U +0000 到 U +ffff | ‘\0’ |
decimal | 128 位精确的十进制值,28-29 有效位数 | (-7.9 x 1028 到 7.9 x 1028) / 100 到 28 | 0.0M |
double | 64 位双精度浮点型 | (+/-)5.0 x 10-324 到 (+/-)1.7 x 10308 | 0.0D |
float | 32 位单精度浮点型 | -3.4 x 1038 到 + 3.4 x 1038 | 0.0F |
int | 32 位有符号整数类型 | -2,147,483,648 到 2,147,483,647 | 0 |
long | 64 位有符号整数类型 | -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807 | 0L |
sbyte | 8 位有符号整数类型 | -128 到 127 | 0 |
short | 16 位有符号整数类型 | -32,768 到 32,767 | 0 |
uint | 32 位无符号整数类型 | 0 到 4,294,967,295 | 0 |
ulong | 64 位无符号整数类型 | 0 到 18,446,744,073,709,551,615 | 0 |
ushort | 16 位无符号整数类型 | 0 到 65,535 | 0 |
2、引用类型
引用类型的变量,变量本身存储的仅仅是实际数据的引用地址,而实际的数据存储在堆上。
(1)、对象(Object
)类型
对象(Object
)类型是 C# 通用类型系统(Common Type System - CTS
)中所有数据类型的终极基类。Object
是 System.Object
类的别名。所以对象(Object
)类型可以被分配任何其他类型(值类型、引用类型、预定义类型或用户自定义类型)的值。但是,在分配值之前,需要先进行类型转换。
-
当一个值类型转换为引用类型时,则被称为"装箱";
-
当一个引用类型转换为值类型时,则被称为”拆箱“。
object obj;
obj = 100; // 装箱
(2)、动态(Dynamic
)类型
该类型可以存储任何类型的值在动态数据类型(Dynamic
)变量中。
(3)、字符串(String
)类型
字符串(String
)类型 允许您给变量分配任何字符串值。字符串(String
)类型是 System.String
类的别名。它是从对象(Object
)类型派生的。
Ⅰ、创建字符串
string str = "Hello, World!";
Ⅱ、字符串连接
string str1 = "Hello, ";
string str2 = "World!";
string combined = str1 + str2; // 结果:"Hello, World!"
Ⅲ、字符串长度
string str = "Hello, World!";
int length = str.Length; // 结果:13
Ⅳ、字符串比较
string str1 = "Hello";
string str2 = "World";
bool areEqual = str1.Equals(str2); // 结果:false
Ⅴ、字符串搜索
string str = "Hello, World!";
int index = str.IndexOf("World"); // 结果:7,不存在则返回-1
Ⅵ、字符串替换
string str = "Hello, World!";
string replaced = str.Replace("World", "C#"); // 结果:"Hello, C#!"
Ⅶ、字符串分割
string str = "Hello, World!";
string[] split = str.Split(',', ' '); // 结果:["Hello", "World!"]
Ⅷ、字符串大小写转换
string str = "Hello, World!";
string lower = str.ToLower(); // 结果:"hello, world!"
string upper = str.ToUpper(); // 结果:"HELLO, WORLD!"
Ⅸ、字符串格式化
string str = "Hello, {0}!";
string formatted = string.Format(str, "World"); // 结果:"Hello, World!"
Ⅹ、字符串插值
string str = $"Hello, {name}!";
(4)、数组(Array
)
数组是一个存储相同类型元素的固定大小的顺序集合。数组是用来存储数据的集合,通常认为数组是一个同一类型变量的集合。
Ⅰ、创建一个数组:
int[] myArray = new int[5]; // 创建一个可以存储5个整数的数组
Ⅱ、初始化数组:
int[] myArray = new int[5] {1, 2, 3, 4, 5}; // 创建并初始化一个有5个元素的数组
Ⅲ、访问数组元素:
int firstElement = myArray[0]; // 获取第一个元素
Ⅳ、修改数组元素:
myArray[0] = 10; // 修改第一个元素为10
Ⅴ、遍历数组:
for(int i = 0; i < myArray.Length; i++)
{
Console.WriteLine(myArray[i]);
}
Ⅵ、数组的长度:
int length = myArray.Length; // 获取数组长度
Ⅷ、多维数组:
int[,] multiDimensionalArray = new int[2,3]; // 创建一个2维数组,2行3列
3、值类型和引用类型的区别
- 一般情况下,值类型的数据存储在栈上,引用类型的数据存储在堆上。
- 值类型的变量,变量本身存储的就是实际的数据;引用类型的变量,变量本身存储的仅仅是实际数据的引用地址,而实际的数据存储在托管堆上。
- 在作为方法的参数进行传递时,值类型参数传递的是值的副本,在方法中对该值进行修改不会影响原始值;引用类型参数传递的是参数的引用地址,在方法中对该参数进行修改会对托管堆上该地址的实际数据进行修改,从而会影响原始值。
- 值类型在作用域内结束时,会被操作系统自释放,减少托管堆压力;引用类型则靠
GC
(C#垃圾回收机器)来进行释放。因此值类型在性能上有优势。
三、运算符
1、算数运算符
运算符 | 描述 |
---|---|
+ | 对两个操作数做加法运算。 |
- | 对两个操作数做减法运算。 |
* | 对两个操作数做乘法运算。 |
/ | 对两个操作数做除法运算。 |
% | 对两个操作数做取余运算。 |
注:
当对两个字符串类型的值使用 + 运算符,代表的是两个字符串值的连接,例如 “123”+“456” 的结果为 “23456” 。
当使用 / 运算符时也要注意操作数的数据类型,如果两个操作数的数据类型都为整数,那么结果相当于取整运算,不包括余数;而两个操作数中如果有一个操作数的数据类型为浮点数,那么结果则是正常的除法运算。
当使用 % 运算符时,如果两个操作都为整数,那么结果相当于取余数。经常使用该运算符来判断某个数是否能被其他的数整除。
2、逻辑运算符(&& || !)
逻辑运算符主要包括与、或、非等,它主要用于多个布尔型表达式之间的运算。
*注:*
在使用逻辑运算符时需要注意逻辑运算符两边的表达式返回的结果都必须是布尔型的。
3、比较运算符
运算符 | 说明 |
---|---|
== | 表示两边表达式运算的结果相等,注意是两个等号 |
=== | (全等) 要求值和数据类型都一致 |
!= | 表示两边表达式运算的结果不相等 |
> | 表示左边表达式的值大于右边表达式的值 |
< | 表示左边表达式的值小于右边表达式的值 |
>= | 表示左边表达式的值大于等于右边表达式的值 |
<= | 表示左边表达式的值小于等于右边表达式的值 |
使用比较运算符运算得到的结果是布尔型的值,因此经常将使用比较运算符的表达式用到逻辑运算符的运算中。
4、三元运算符(由称三元表达式)
布尔表达式 ? 表达式 1: 表达式 2
其中:
- 布尔表达式:判断条件,它是一个结果为布尔型值的表达式。
- 表达式 1:如果布尔表达式的值为
True
,该三元运算符得到的结果就是表达式 1 的运算结果。 - 表达式 2:如果布尔表达式的值为
False
,该三元运算符得到的结果就是表达式 2 的运算结果。
5、赋值运算符
运算符 | 说 明 |
---|---|
= | x = y,等号右边的值给等号左边的变量,即把变量 y 的值赋给变量 x |
+= | x += y,等同于 x = x + y |
-= | x -= y,等同于 x = x - y |
*= | x = y,等同于 x = x * y |
/= | x /= y,等同于 x = x / y |
%= | x %= y,等同于 x = x % y,表示求 x 除以 y 的余数 |
++ | x++ 或 ++x,等同于 x = x + 1 |
– | x-- 或 --x,等同于 x = x - 1 |
注:
++ 和 – 运算符放在操作数前和操作数后是有区别的:
- 如果放在操作数前,需要先将操作数加 1 或减 1,然后再与其他操作数进行运算;
- 如果放在操作数后,需要先与其他操作数进行运算,然后操作数自身再加 1或减 1。
四、方法参数
1、引用参数和值参数
Ⅰ、值参数(Value Parameters
)
使用值参数时,方法接收的是参数的副本,对参数的修改不会影响原始值。值参数使用时,参数前面不需要加任何修饰符,默认就是传递的是值参数。
void ModifyValue(int x)
{
x = x + 10;
Console.WriteLine(x); // 输出 15
}
int num = 5;
ModifyValue(num); // 输出 15
Console.WriteLine(num); // 输出 5
Ⅱ、引用参数(Reference Parameters)
使用引用参数时,方法接收的是参数的引用(参数的本身),对参数的修改会影响原始值。引用参数使用 ref
关键字修饰。
- 值类型的引用参数
void ModifyReference(ref int x)
{
x = x + 10;
Console.WriteLine(x); // 输出 15
}
int num = 5;
ModifyReference(ref num); // 输出 15
Console.WriteLine(num); // 输出 15
- 引用类型的引用参数
static void Main(string[] args)
{
People people = new People();
people.Name = "C#初级程序员";
Console.WriteLine("{0},{1}", people.GetHashCode(), people.Name); // 46104728,C#初级程序员
Console.WriteLine("方法调用前---------------------------");
TestRef(ref people); // 12289376,C#高级程序员
Console.WriteLine("{0},{1}", people.GetHashCode(), people.Name); // 12289376,C#高级程序员
Console.ReadLine();
}
static void TestRef(ref People people) {
people = new People() { Name = "C#高级程序员" };
Console.WriteLine("{0},{1}", people.GetHashCode(), people.Name);
}
通过引用参数,方法可以直接修改原始变量的值,而不是操作变量的副本。需要注意的是,在调用使用引用参数的方法时,参数也需要使用 ref
关键字修饰。
2、out
输出参数
out
输出参数是 C# 中的一种参数传递方式,用于向方法传递一个参数,并要求方法在返回之前对该参数进行赋值。
static void Main(string[] args)
{
bool b = TestOut("15688963113", out double x);
Console.WriteLine(x);
Console.ReadLine();
}
/// <summary>
/// 输出参数示例
/// </summary>
/// <param name="result"></param>
static bool TestOut(string value, out double result) {
try
{
result = double.Parse(value); // 输出参数在方法返回前必须赋值
return true;
} catch
{
result = 0;
throw;
}
}
3、数组参数
params
数组参数(也称为可变参数)可以来向方法传递可变数量的参数。
- 数组参数允许您以一种更灵活的方式定义和调用方法,而无需提前指定参数的数量。例如:
string.Format();
- 数组参数必需是方法形参的最后一个。
TestParams(1, 2, 3, 4, 5);
static int TestParams(params int[] nums) {
int total = 0;
foreach (var item in nums)
{
total += item;
}
return total;
}
4、可选参数
在 C# 中,可选参数是指,在定义方法时,可以为某些参数指定默认值,使得在调用该方法时可以选择性地省略这些参数,编译器会自动使用该参数的默认值,如果提供了对应参数的值,则会覆盖默认值。可选参数可以显著低降低重载的数量。
static void Main()
{
PrintMessage("Hello"); // 调用方法时只提供一个参数
PrintMessage("Hello", 3); // 调用方法时提供两个参数
}
// 默认只输出一次
static void PrintMessage(string message, int times = 1)
{
for (int i = 0; i < times; i++)
{
Console.WriteLine(message);
}
}
5、扩展方法(this
参数)
C# 中的扩展方法是一种特殊的静态方法,它允许你向现有的类添加新的方法,而无需修改原始类的代码。通过扩展方法,你可以为任何现有的类(包括 .NET 框架中的类)添加新的方法,从而使得代码更加灵活和易于扩展。C#中的Linq
就属于扩展方法。
要创建一个扩展方法,需要满足以下条件:
- 扩展方法必须定义在一个静态类中。
- 扩展方法必须是静态的,并且第一个参数指定要扩展的类型,使用 this 关键字标识。
- 扩展方法不能修改原始类的定义,只能添加新的方法。
以下是一个示例,演示如何为 string
类型添加一个扩展方法 Reverse()
,用于反转字符串:
class Program
{
static void Main()
{
string original = "Hello";
Console.WriteLine(original.Reverse()); // 输出 "olleH"
}
}
public static class StringExtensions {
/// <summary>
/// 扩展方法
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public static string Reverse(this string input) {
char[] chars = input.ToCharArray(); // 将字符串转换成字符数组
char[] result = chars.Reverse().ToArray(); // 反转数组
return string.Join("", result);
}
}
五、委托 delegate
1、概念
委托的本质是一种类型安全的函数指针。
在 C# 中,委托(Delegate
)是一种类型,它可以存储对方法的引用,允许将方法作为参数传递给其他方法,或者将方法作为返回值返回。委托可以用来实现事件处理、回调函数等功能。
2、自定义委托
using System;
// 定义一个委托
delegate void MyDelegate(string message); // 委托中的参数为目标方法的参数列表
class Program
{
static void Main()
{
// 实例化委托,存储或指向要调用的方法
MyDelegate delegateInstance = new MyDelegate(PrintMessage);
/*
编译器会自动将方法包装成适当的委托类型,这是因为C#具有一种称为"隐式委托转换"的特性。
MyDelegate delegateInstance = PrintMessage;
*/
// 调用委托
delegateInstance("Hello, World!");
}
// 委托需要调用的方法
static void PrintMessage(string message)
{
Console.WriteLine(message);
}
}
3、内置委托(Func
和 Action
)
(1)、Func
委托
Func
委托表示一个具有特定参数和返回值类型的方法。Func
委托是一个泛型委托,它可以接受最多 16 个输入参数,并且必须有一个返回值。最后一个类型参数表示方法的返回值类型。
- 普通方法
Func<double, double, double> funcAdd = Add;
Console.WriteLine("Func泛型委托示例:" + funcAdd(3, 5)); // Func泛型委托示例:300
// 计算值的合
static double Add(double x, double y) {
return x + y;
}
lambda
表达式(匿名方法)
// 例子:接受两个 int 类型参数并返回一个 int 类型值的委托
Func<int, int, int> add = (x, y) => x + y;
int result = add(3, 5); // 结果为 8
(2)、Action
委托
Action
委托表示一个不返回值的方法。它也是一个泛型委托,可以接受最多 16 个输入参数,没有返回值。Action
委托通常用于表示不需要返回值的操作或事件处理。
- 普通方法
Action<string> actionPrint = PrintMessage;
actionPrint("Hello, World!");
static void PrintMessage(string message)
{
Console.WriteLine(message);
}
lambda
表达式(匿名方法)
// 例子:接受一个 string 类型参数并打印输出的委托
Action<string> printMessage = (message) => Console.WriteLine(message);
printMessage("Hello, World!"); // 输出 "Hello, World!"
总结 :
Func
委托用于表示具有返回值的方法,最后一个类型参数表示返回值类型。Action
委托用于表示不返回值的方法,通常用于执行操作或事件处理。
(4)、委托和Lambda
表达式的示例
Calculate<double>((x, y) => x + y, 100, 200);
/// <summary>
/// 计算值
/// </summary>
/// <typeparam name="T">泛型类型</typeparam>
/// <param name="func">接受两个泛型参数并返回一个泛型结果的`Func`委托</param>
/// <param name="x"></param>
/// <param name="y"></param>
static void Calculate<T>(Func<T, T, T> func, T x, T y) {
Console.WriteLine(func(x, y));
}
4、LINQ
语句
(1)、概念
C# 语言中的 LINQ(Language Integrated Query)
是一种查询语言,用于方便地查询各种数据源,如集合、数据库、XML等。LINQ
提供了一种统一的编程模型,使开发人员能够使用类似SQL
的语法来查询数据。
在 C# 中使用LINQ
,需要引入System.Linq
命名空间。
(2)、LINQ
、委托(Delegates
)和Lambda
表达式的关系
Ⅰ、三者的作用以及关系
- 委托:
委托是 C# 中的一种类型,用于表示对方法的引用。委托可以被用来封装方法,并且可以像其他数据类型一样被传递和调用。
在LINQ
中,委托经常用于传递给LINQ
方法,如Where、Select
等,以指定查询条件或转换逻辑。
示例中 ‘ W h e r e ‘ 方法的参数是 ‘ F u n c < d o u b l e , b o o l > ‘ 委托类型,该委托的参数是 ‘ d o u b l e ‘ ,返回值是 ‘ b o o l ‘ {\color{red}示例中`Where`方法的参数是 `Func< double, bool >`委托类型,该委托的参数是`double`,返回值是`bool`} 示例中‘Where‘方法的参数是‘Func<double,bool>‘委托类型,该委托的参数是‘double‘,返回值是‘bool‘。
List<double> list = new List<double>() { 1, 2, 3, 4, 5, 6, 7 };
var newList = list.Where(x => x >= 2).ToList();
Console.WriteLine(string.Join(",", newList));
Lambda
表达式
Lambda
表达式是一种匿名函数,它允许你定义一个简短的函数体,通常用于传递给委托或作为LINQ查询的条件。
Lambda表达式的语法形式为 (input parameters) => expression
,其中箭头左侧是输入参数列表,右侧是表达式或语句块。
Lambda
表达式可以替代传统的委托定义,使代码更加简洁和易读。(编译器会自动将方法包装成适当的委托类型,这是因为C#具有一种称为"隐式委托转换"的特性。)
LINQ
LINQ
是Language Integrated Query
的缩写,它是 C# 中用于查询数据的一种语言集成查询技术。LINQ
提供了一种统一的编程模型,使得对各种数据源进行查询变得更加方便。
LINQ
方法通常接受一个委托作为参数,用于指定查询条件、转换逻辑等。Lambda
表达式经常被用来创建这些委托,使得代码更加简洁和易读。
Ⅱ、常用的LINQ
LINQ(Language Integrated Query)
是C#中用于查询数据的功能,它提供了一组丰富的查询操作符,可以用于查询、过滤、排序、分组等操作。以下是一些常用的LINQ
语句及其功能:
①、Select:用于从集合中选择指定的元素或对元素进行转换。
var names = people.Select(p => p.Name);
②、Where:用于根据指定的条件筛选集合中的元素。
var adults = people.Where(p => p.Age >= 18);
③、OrderBy:用于对集合中的元素进行排序。
var sortedNames = names.OrderBy(n => n);
④、GroupBy:用于根据指定的键对集合中的元素进行分组。
var groupedPeople = people.GroupBy(p => p.City);
⑤、Join:用于连接两个集合并返回一个结果集。
var result = people.Join(cities, p => p.CityId, c => c.Id, (p, c) => new { p.Name, c.CityName });
⑥、Any:用于检查集合中是否存在满足指定条件的元素。
bool hasAdult = people.Any(p => p.Age >= 18);
⑦、Count:用于计算集合中满足指定条件的元素个数。
int adultCount = people.Count(p => p.Age >= 18);
⑧、Sum:用于计算集合中数值类型元素的和。
int totalAge = people.Sum(p => p.Age);
⑨、Average:用于计算集合中数值类型元素的平均值。
double averageAge = people.Average(p => p.Age);
这些是一些常用的LINQ语句,可以帮助你对数据进行灵活、高效的查询和操作。通过组合不同的LINQ操作符,可以实现丰富的数据处理功能。希望这些示例能帮助你更好地理解LINQ的应用。如果有任何疑问,请随时提出。
5、多播委托
在 C# 中,多播委托是一种特殊类型的委托,它可以持有对多个方法的引用,并且可以依次调用这些方法。多播委托实际上是一个委托链。
-
定义多播委托:要定义一个多播委托,首先需要声明一个委托类型,然后可以使用该委托类型来创建一个多播委托变量。多播委托的声明方式与单播委托相同,只是在赋值时可以使用
+=
运算符来添加多个方法引用。 -
添加方法到多播委托:可以使用
+=
运算符将一个或多个方法添加到多播委托中。每次添加方法时,实际上是将该方法的引用添加到委托链的末尾。 -
从多播委托中移除方法:可以使用
-=
运算符从多播委托中移除方法。移除方法时,会从委托链中删除该方法的引用。 -
调用多播委托:当调用多播委托时,它会按照方法添加的顺序依次调用每个方法。每个方法都会接收相同的参数,并且可以返回相同或不同的类型。
public delegate void MyDelegate(string message); // 声明一个委托
public static void Main()
{
MyDelegate myDelegate = Method1; // 创建一个委托实例,并给委托实例添加方法的引用
myDelegate += Method2; // 使用 += 运算符将多个方法的引用添加到多播委托中
myDelegate += Method3;
myDelegate -= Method1; // 从委托链中移除该方法的引用
myDelegate("Hello, World!"); // 调用多播委托
}
public static void Method1(string message)
{
Console.WriteLine("Method 1: " + message);
}
public static void Method2(string message)
{
Console.WriteLine("Method 2: " + message);
}
public static void Method3(string message)
{
Console.WriteLine("Method 3: " + message);
}
6、委托的一般使用
(1)、工厂模板方法
class DelegateProgram
{
static void Main(string[] args)
{
FactoryTemplate factoryTemplate = new FactoryTemplate(); // 创建工厂类,用于生产包装产品
Behavior behavior = new Behavior(); // 创建流水线类,用于创建(生产)产品
// 生产产品,并包装到盒子中
Box box = factoryTemplate.Production(new Func<Procedure>(behavior.MakeCar));
/*
Box box = factoryTemplate.Production(behavior.MakeCar);
编译器会自动将方法包装成适当的委托类型,这是因为C#具有一种称为"隐式委托转换"的特性。
隐式委托转换允许编译器在需要委托类型的地方自动将方法引用转换为相应的委托类型,而无需显式地创建委托实例。
*/
Console.WriteLine(box.ProcedureFile.Name);
Console.ReadLine();
}
}
/// <summary>
/// 产品
/// </summary>
class Procedure {
public string Name { get; set; }
}
/// <summary>
/// 盒子
/// </summary>
class Box {
public Procedure ProcedureFile { get; set; }
}
/// <summary>
/// 工厂(包装产品)
/// </summary>
class FactoryTemplate {
/// <summary>
/// 包装产品,模板方法
/// </summary>
/// <param name="func">生产产品的方法</param>
/// <returns></returns>
public Box Production(Func<Procedure> func) {
Box box = new Box();
box.ProcedureFile = func();
return box;
}
}
/// <summary>
/// 流水线(用于生产产品)
/// </summary>
class Behavior {
/// <summary>
/// 生产产品
/// </summary>
/// <returns></returns>
public Procedure MakeCar() {
return new Procedure() { Name = "小汽车" };
}
}
FactoryTemplate
类是一个工厂模板类,用于包装产品。它包含了一个Production
方法,该方法接受一个Func<Procedure>
类型的委托参数,用于生产产品并将其包装到盒子中。Behavior
类是一个流水线类,用于创建(生产)产品。它包含了一个MakeCar
方法,用于生产一种Nmae
为“小汽车”的产品。Procedure
类表示产品,其中包含一个Name
属性用于存储产品的名称。Box
类表示盒子,其中包含一个ProcedureFile
属性用于存储盒子中的产品。- 在
Main
方法中,首先创建了一个FactoryTemplate
实例和一个Behavior
实例。 - 然后通过
factoryTemplate.Production(new Func<Procedure>(behavior.MakeCar))
调用工厂模板的Production
方法,传入behavior.MakeCar
方法作为参数。这里使用了委托来将生产产品的方法传递给工厂类。 - 工厂类会调用传入的委托方法来生产产品,并将产品放入盒子中。最后,打印出盒子中产品的名称。
7、委托与接口的区别
下面是一些比较接口和委托的情况:
使用接口的优势:
- 强类型约束:接口可以提供更严格的类型约束,确保实现类符合指定的接口规范。
- 多态性:接口支持多态性,允许不同类实现相同的接口并提供不同的实现。
- 类的继承:接口可以与类的继承结合使用,实现更复杂的类层次结构。
- 抽象行为:接口定义了一组抽象行为,使得代码更易于理解和维护。
- 依赖注入:接口可以用于实现依赖注入,提高代码的可测试性和可扩展性。
使用委托的优势:
-
灵活性:委托更灵活,可以在运行时动态指定方法,而不需要提前定义接口。
-
匿名方法:委托支持匿名方法和 Lambda 表达式,使得代码更简洁和易读。
-
事件处理:委托更适合实现事件处理机制,简化了事件的订阅和通知过程。
-
单一职责原则:委托可以帮助实现单一职责原则,将不同的行为分离开来,提高代码的可维护性。
综上所述:
接口适合定义类之间的契约和抽象行为,适用于更静态、结构化的设计;
而委托适合实现回调函数、事件处理和动态行为,适用于更灵活、动态的场景。
在实际开发中,可以根据具体需求和设计目标选择合适的方式来实现功能。有时候,接口和委托也可以结合使用,以发挥它们各自的优势。