C#学习笔记-基础篇

一、基本语法

1.1、using关键字

using关键字具有多种用途,但最常用于两个场景:管理资源(如文件句柄、数据库连接等)和引入命名空间。这两个场景在提高代码的可读性和维护性方面起到了关键作用。

1.1.1、资源管理

using语句是用来自动管理资源的,确保资源在使用后被正确释放。这在处理非托管资源时尤为重要,例如文件句柄、数据库连接等。当using块结束时,资源将自动关闭和释放。

using (StreamWriter writer = new StreamWriter("example.txt"))  
{  
    writer.WriteLine("Hello, World!");  
} // 这里的代码块结束时,StreamWriter会自动关闭文件流。

1.1.2、引入命名空间

using关键字还可以用来引入命名空间,这样你就可以不必每次都写出完整的命名空间,从而使得代码更加简洁。

using System.IO; // 引入System.IO命名空间,这样就可以直接使用File和Directory类,而不需要写出完整的命名空间。

1.2、class关键字

class关键字是用来声明一个类的基础。类是面向对象编程的核心概念之一,它定义了一组属性、方法和事件,这些构成了对象的蓝图。理解如何使用class关键字对于C#开发人员来说至关重要。

1.3、注释

注释是编程中非常重要的部分,它们为代码提供了文档和解释,帮助开发者理解代码的工作原理。在C#中,注释主要有两种类型:单行注释和多行注释。

1.3.1、单行注释

单行注释以//开头,并延续到该行的末尾。它们通常用于简单的说明或临时禁用某行代码。

// 这是一个单行注释  
int x = 5; // x被赋值为5

1.3.2、多行注释

多行注释以/*开头,并以*/结尾。它们用于为代码块提供详细的说明或暂时禁用多行代码。

/* 这是一个  
多行注释,它可以跨越多行 */  
int y = 10; /* y被赋值为10 */

1.4、成员变量

成员变量是定义在类或结构体中的变量,用于存储类的状态信息。成员变量可以是公共的(public)、受保护的(protected)、私有的(private)或内部的(internal),具体取决于它们的访问修饰符。

1.4.1、声明方式

成员变量的声明方式通常是在类的内部,位于方法之外。声明时需要指定变量的数据类型和变量名。例如:

public class MyClass  
{  
    public int MyVariable; // 公共成员变量  
    private int MyPrivateVariable; // 私有成员变量  
}

1.4.2、作用域和生命周期

  1. 作用域:成员变量的作用域取决于它们的访问修饰符。如果成员变量是公共的,则可以在类的任何地方访问它。如果成员变量是受保护的或私有的,则只能在它们所属的类或派生类中访问。如果成员变量是内部的,则只能在同一个程序集中的类中访问。
  2. 生命周期:成员变量的生命周期与对象的生命周期相同。当对象被创建时,成员变量将自动分配内存并初始化为默认值(对于数值类型是0,对于引用类型是null)。当对象被垃圾回收时,成员变量将自动释放所占用的内存。

1.4.3、初始化和赋值

在类的构造函数或其他方法中,可以对成员变量进行初始化和赋值操作。例如:

public class MyClass  
{  
    public int MyVariable;  
      
    public MyClass() // 构造函数  
    {  
        MyVariable = 10; // 初始化成员变量  
    }  
}

1.5、成员函数

成员函数是定义在类或结构体中的函数,用于实现类的各种操作和行为。成员函数可以访问类的成员变量和其他的成员函数。

1.6、实例化一个类

实例化一个类需要使用关键字“new”,并指定类的名称和参数列表(如果有的话)。

public class MyClass
{
    
}
Myclass a=new MyClass();//实例化类

1.7、标识符

标识符是用于标识变量、常量、类、方法等程序元素的名称。在C#中,标识符由字母、数字和下划线组成,并且必须以字母或下划线开头。

1.7.1、命名规则

  1. 标识符的长度可以由任意个字符组成,但通常不超过255个字符。
  2. 标识符只能包含字母、数字和下划线。
  3. 标识符不能是C#的关键字或保留字。
  4. 标识符应具有描述性,以便其他开发人员能够理解其用途。
  5. 标识符应避免使用C#的保留字,例如class、public、void等。

1.8、C#关键字

C#语言中有许多关键字,它们具有特殊含义,被C#编译器用作指令或标识符。关键字是预定义的,不能用作变量名、类名或其他标识符。以下是C#中的一些常用关键字:

  1. public:表示成员可以从任何其他类或方法访问。
  2. private:表示成员只能在声明它的类或结构体中访问。
  3. protected:表示成员可以在声明它的类或派生自该类的子类中访问。
  4. internal:表示成员只能在声明它的程序集内部访问。
  5. protected internal:表示成员可以在声明它的类或派生自该类的子类,以及程序集内部的其他部分中访问。
  6. private protected:表示成员只能在声明它的类或派生自该类的子类中访问,且不能从程序集外部访问。
  7. static:表示成员属于类型本身,而不是类型的特定实例。
  8. void:用于表示方法不返回任何值。
  9. class:用于声明一个类。
  10. interface:用于声明一个接口。
  11. struct:用于声明一个结构体。
  12. enum:用于声明一个枚举。
  13. public enum:表示枚举成员可以从任何其他类或方法访问。
  14. protected enum:表示枚举成员只能在声明它的类或派生自该类的子类中访问。
  15. private enum:表示枚举成员只能在声明它的类中访问。
  16. static class:表示一个只包含静态成员的类。
  17. sealed:表示一个类不能被继承,一个方法不能被重写。
  18. override:表示一个方法重写基类中的虚方法或抽象方法。
  19. abstract:用于声明抽象类或抽象成员。
  20. extern:用于声明一个外部方法,其实现由非托管代码提供。
  21. readonly:表示一个字段只能在声明或在构造函数中赋值。
  22. asyncawait:用于异步编程模型。
  23. sizeof:获取给定类型的字节大小。
  24. typeof:获取给定类型的Type对象。
  25. checkedunchecked:用于整数溢出检查控制。
  26. default:用于获取类型的默认值。
  27. yield:用于定义返回迭代器的简单方法。
  28. using:用于确保资源在使用后被正确释放。
  29. lock:确保某代码块在给定时刻只能由一个线程访问。
  30. delegate:用于定义委托类型,代表可调用的方法引用类型。
  31. var:用于自动类型推断变量声明。
  32. dynamic:用于在运行时解析类型信息。
  33. where:用于约束泛型参数的类型。
  34. yield break:用于在迭代器方法中提前终止迭代。
  35. base:用于访问基类中的成员或调用基类中的构造函数。
  36. this:用于引用当前实例的成员或构造函数。
  37. event:用于声明事件,允许外部类向当前类发送通知。
  38. break, continue, return, 和 throw: 控制流程的关键字。
  39. default(T)null: 用于泛型和引用类型的相关操作。
  40. async void: 通常不推荐在异步编程中使用,因为它可能会导致意外的行为和错误处理问题。
  41. awaitable void: 表示一个返回void的方法可以被异步调用,但通常不推荐这种做法,因为它是破坏性的,并可能导致未捕获的异常等问题。

1.9、c#占位符

占位符通常用于格式化字符串,它允许你在字符串中指定位置,以便稍后插入变量或表达式的值。占位符通常与string.Format方法或字符串插值(C# 6.0及更高版本)一起使用。

1.9.1、使用string.Format方法

string.Format方法允许你创建一个包含占位符的字符串,然后提供一个或多个对象来替换这些占位符。占位符使用大括号 {} 来指定,并在大括号内使用索引来表示参数的位置。

string name = "Alice";  
int age = 30;  
string formattedString = string.Format("My name is {0} and I am {1} years old.", name, age);  
Console.WriteLine(formattedString); // 输出: My name is Alice and I am 30 years old.

1.9.2、使用字符串插值(C# 6.0+)

从C# 6.0开始,你可以使用字符串插值来更简洁地插入变量或表达式的值。字符串插值使用 $ 符号和 {} 来包含要插入的表达式。

string name = "Bob";  
int age = 25;  
string interpolatedString = $"My name is {name} and I am {age} years old.";  
Console.WriteLine(interpolatedString); // 输出: My name is Bob and I am 25 years old.

1.9.3、自定义格式化

除了插入变量的值外,你还可以使用占位符来指定自定义格式。通过在占位符中添加格式说明符,你可以控制数字的格式、日期的格式等。

double money = 1234.56;  
string formattedMoney = string.Format("The price is {0:C}.", money); // C表示货币格式  
Console.WriteLine(formattedMoney); // 输出: The price is $1,234.56. (假设区域设置为美国)

二、数据类型

2.1、值类型

值类型是C#中的一种基础数据类型,它们直接包含数据,与引用类型相对。值类型变量直接存储数据,而引用类型变量则存储对数据的引用。

以下是关于C#值类型的一些关键点:

  1. 基本值类型
    • int: 32位整数。
    • long: 64位整数。
    • short: 16位整数。
    • byte: 8位无符号整数。
    • float: 单精度浮点数。
    • double: 双精度浮点数。
    • bool: 布尔值,可以是true或false。
    • char: 16位Unicode字符。
  2. 枚举类型 (Enum): 枚举是一种特殊的值类型,它允许你为一组相关的常量定义有意义的名称。
  3. 结构类型 (Struct): 结构是值类型,类似于类,但结构的大小是确定的,并且不能被继承。
  4. 可空类型 (Nullable): 对于值类型,你可以使用Nullable<T>特性来允许该类型的值为null。例如,int?可以有一个值,也可以是null。
  5. 默认值:值类型的默认值是0(对于数值类型)或null(对于引用类型)。对于结构体,默认值是所有字段的默认值。
  6. 装箱与拆箱:当值类型被赋给引用类型的变量时,会发生装箱操作;相反,当从引用类型中取出值并赋给值类型的变量时,会发生拆箱操作。
  7. 传递方式:由于值类型直接包含数据,因此它们是通过值传递的。这意味着当一个值类型的变量被传递给方法时,实际上传递的是该变量的副本,而不是对原始数据的引用。这意味着方法无法修改传递的值类型的变量的实际数据,因为原始数据和副本是独立的。
  8. 装箱值类型:对于非托管的堆栈上的对象,如结构体或数组,可以使用装箱将其转换为引用类型。这允许这些对象通过引用来传递或返回。
  9. 值类型的比较:由于值类型直接包含数据,所以它们的比较通常是快速的、简单的内存比较。
  10. 性能考虑:由于值类型存储的是实际数据而不是引用,因此在处理大量数据时,使用值类型通常比引用类型更高效。

2.2、引用类型

引用类型在C#中是指向对象内存地址的引用,而不是实际的对象。这意味着引用类型变量存储的是对象的内存地址,而不是对象本身。

以下是关于C#引用类型的一些关键点:

  1. 引用类型基础
    • 引用类型变量存储的是对象的内存地址,而不是实际的对象。
    • 引用类型变量可以被赋值为null,表示没有指向任何对象。
  2. 类 (Class): 这是最主要的引用类型。类是一种用户定义的数据类型,它允许你组合数据(字段)和行为(方法)。
  3. 数组:数组是引用类型,因为它们包含对象(数组中的元素)。一个数组变量实际上是该数组第一个元素的内存地址。
  4. 字符串 (String): 字符串通常被视为特殊的引用类型,因为它们是不可变的,并且存储在内存中的特殊区域。
  5. 委托 (Delegate): 委托是一种特殊的引用类型,用于封装方法作为参数传递或赋值给变量。
  6. 接口 (Interface): 接口定义了一组方法的契约,但不包含实现。实现接口的类必须提供这些方法的实现。
  7. 事件 (Event): 事件是一种特殊的委托,用于在类或对象之间进行通知。
  8. 弱引用 (WeakReference): 弱引用允许垃圾回收器在没有任何强引用指向对象时释放该对象。
  9. 强引用 (StrongReference): 这是默认的引用类型,强引用阻止垃圾回收器回收被引用的对象。
  10. 复制 vs. 传递:由于引用类型的变量存储的是对象的内存地址,所以当传递引用类型的变量给方法时,实际上传递的是对该内存地址的引用,而不是对象的副本。这意味着方法可以修改传递的对象(如果具有适当的访问权限)。
  11. 垃圾回收:对于托管堆上的对象(由.NET运行时管理的),垃圾回收器负责回收不再使用的内存。如果一个引用类型的变量不再被任何变量所引用,其关联的对象就会被标记为可回收的。
  12. 性能考虑:由于引用类型变量存储的是内存地址,而不是实际的数据,所以它们通常比值类型需要更多的内存。此外,由于可能涉及到垃圾回收和内存管理,使用引用类型可能会影响应用程序的性能。

2.3、值类型和引用类型的存储方式

值类型直接存储数据在栈内存中,而引用类型存储的是对托管堆上对象的内存地址的引用。这种区别影响了它们的传递方式、性能以及垃圾回收的处理方式。

2.4、var关键字

var 用于声明一个局部变量,而不需要明确指定变量的类型。编译器会自动推断变量的类型,根据初始化该变量的表达式的类型。

使用 var 可以简化代码,特别是在变量类型明确且从初始化表达式中可以推断出来的情况下。

var number = 10; // 编译器推断为 int 类型  
var name = "Alice"; // 编译器推断为 string 类型  
var isFish = true; // 编译器推断为 bool 类型  
var point = new Point(1, 2); // 编译器推断为 Point 类型(假设存在一个Point类)

编译器通过分析右侧的初始化表达式来推断 var 声明的变量类型。这个特性特别适用于那些类型是隐含的或者从上下文中可以明确推断出来的场合。

需要注意的是,var 只能用于局部变量,不能用于字段、方法参数或方法返回类型。在这些情况下,必须显式声明变量类型。

三、C#类型转换

3.1、隐式转换 (Implicit Casts):

  • 当转换是安全的,不需要显式调用转换操作符时,可以使用隐式转换。
  • 例如,从派生类到基类的转换是隐式的。
class BaseClass {}  
class DerivedClass : BaseClass {}  
DerivedClass derived = new DerivedClass();  
BaseClass baseClass = derived; // 隐式转换

3.2、显式转换 (Explicit Casts):

  • 当需要进行不安全的转换时,需要使用显式转换操作符。
  • 例如,从基类到派生类的转换通常需要显式转换。
BaseClass baseClass = new BaseClass();  
DerivedClass derived = (DerivedClass)baseClass; // 显式转换

3.3、使用as关键字进行转换(不安全):

  • 当尝试进行不安全的转换时,可以使用as关键字尝试进行转换,如果转换失败则返回null。
BaseClass baseClass = new BaseClass();  
DerivedClass derived = baseClass as DerivedClass; // 使用as关键字尝试转换  
if (derived != null) {  
    // 转换成功  
} else {  
    // 转换失败  
}

3.4、装箱与拆箱

对于值类型,装箱是将值类型转换为引用类型的过程;拆箱则是相反的过程。装箱通常涉及在托管堆上为值类型创建一个新的对象实例,并返回对该实例的引用。拆箱则是获取该引用的实际值类型。

3.5、强制类型转换与Convert:

Convert类提供了一些静态方法,用于执行各种类型之间的转换,如ToInt32, ToDouble等。这些方法在内部使用装箱和拆箱操作。

3.6、自定义类型转换

可以为自定义类型定义类型转换器,通过实现IConvertible接口或定义explicitimplicit的静态方法来实现。

3.7、异常处理

类型转换可能会引发异常,如InvalidCastException,因此应始终在尝试类型转换时进行异常处理。

3.8、使用dynamic类型

在C# 4.0及更高版本中,可以使用dynamic关键字来进行类型不安全(运行时确定)的类型转换,这种转换会延迟到运行时才进行类型检查。

3.9、使用泛型方法

通过定义泛型方法,可以编写适用于多种数据类型的通用代码,而无需进行显式的类型转换。

3.10、注意事项

在进行类型转换时,应确保目标类型的范围足够大以容纳源类型的值(例如,将int转换为short可能会导致数据丢失),并确保源对象实际上是目标类型的实例或子类的实例。

3.11、溢出检查

使用溢出检查来确保在执行算术运算或类型转换时不会发生溢出。例如,使用checked关键字或调用Math.checked方法。

3.12、C#中的类型强制转换

对于值类型和引用类型的强制转换,可以使用C#中的强制类型转换操作符((type))。对于值类型,这可能会导致数据截断或溢出;对于引用类型,这可能不会更改实际对象的内存地址(浅拷贝),或可能导致新对象的创建(深拷贝)。

四、常量

常量是在编译时就已经确定且不能更改的值

4.1、整数常量

整数常量在C#中表示整数值,可以是正数、负数或零。

  1. 基本数据类型
    • byte: 8位无符号整数,取值范围是0-255。
    • short: 16位有符号整数,取值范围是-32768到32767。
    • int: 32位有符号整数,取值范围是-2147483648到2147483647。
    • long: 64位有符号整数,取值范围是-9223372036854775808到9223372036854775807。
  2. 字面值:整数常量是在代码中直接表示整数值的方式。例如:123-4560等。
  3. 十六进制字面值:以0x0X开头的整数常量表示十六进制值。例如:0x1A0XfF
  4. 八进制字面值:以0开头的整数常量表示八进制值。例如:010077
  5. 二进制字面值:C# 7.0及更高版本支持以_分隔的二进制形式表示整数常量。例如:0b1010_1011
  6. 整数常量的后缀:可以添加后缀来指定字面值的类型,如dD表示十进制,uU表示无符号,lL表示长整数。例如:1234567890l是一个长整数字面值。
  7. 整数的默认值:对于局部变量,如果未初始化,整型变量的默认值是0。对于类的字段,如果未显式初始化并且没有使用默认值,则它们会被自动初始化为默认值(对于数值类型,默认值是0或0.0)。
  8. 整数溢出:当整数超出了其类型的取值范围时,会发生溢出,这通常会导致数据丢失或异常行为。为了避免溢出,可以使用溢出检查运算符(如checked)或在代码中显式检查边界条件。
  9. 整数的位表示:在C#中,可以使用位运算符来操作整数在内存中的二进制表示。例如,使用位移运算符(<<和>>)来执行左移和右移操作。
  10. 整数在算术运算中的行为:整数在算术运算中遵循标准的数学规则,包括加法、减法、乘法和除法等。但是,除法可能会因为溢出或除以零而引发异常。

4.2、浮点常量

浮点常量用于表示实数,即带有小数点的数值。

  1. 基本数据类型
    • float: 32位单精度浮点数,大约可以表示7位有效数字。
    • double: 64位双精度浮点数,大约可以表示15-16位有效数字。
    • decimal: 128位的高精度浮点数,常用于需要高精度计算的金融和货币计算。
  2. 字面值:浮点常量可以用小数形式表示,例如:3.14-0.001123.456等。
  3. 科学记数法:可以使用科学记数法来表示非常大或非常小的浮点数。例如:1.23e-4表示1.23×10−4。
  4. 后缀:浮点字面值可以添加后缀来指定类型,例如:fF表示float类型,dD表示double类型,mM表示decimal类型。如果不指定后缀,则默认为double类型。例如:3.14F是一个float类型的浮点数,而3.14M是一个decimal类型的浮点数。
  5. 精度和范围:不同类型的浮点数具有不同的精度和范围。双精度浮点数(double)具有更高的精度和更大的范围,但也需要更多的内存和计算资源。单精度浮点数(float)的精度和范围较小,但通常足够用于一般计算。高精度浮点数(decimal)提供了更高的精度,适用于需要精确计算的场景,如货币计算。
  6. 浮点数的比较:由于浮点数的精度问题,直接比较两个浮点数可能会导致不期望的结果。建议使用近似比较的方法,例如比较两个数的差值是否小于某个很小的阈值。
  7. NaN和无穷大:C#中的浮点数支持特殊值NaN(Not a Number,非数字)和无穷大(Positive Infinity和Negative Infinity)。这些特殊值用于表示无法表示的数或计算结果超出浮点数范围的情况。
  8. 格式化输出:可以使用格式化字符串来控制浮点数的输出格式。例如,使用String.Format方法或ToString方法,可以指定小数点后的位数、千位分隔符等。
  9. 类型转换:在C#中,可以将一种浮点类型转换为另一种浮点类型。转换时可能会发生精度损失或溢出。可以使用显式转换或隐式转换来完成类型转换操作。

4.3、字符常量

字符常量用于表示单个字符。

  1. 基本数据类型
    • char: 表示一个Unicode字符。
  2. 字面值:字符常量使用单引号括起来表示。例如:'a''B''1'等。
  3. 转义字符:某些字符有特殊的含义,例如换行符\n、制表符\t等。在字符常量中,如果要表示这些特殊字符,需要使用转义序列。例如:'\n'表示换行符。
  4. Unicode字符:可以使用Unicode码位来表示字符。例如:'\u0041'表示大写字母A。
  5. 字符常量的范围:由于char类型是基于Unicode的,它可以表示从U+0000到U+FFFF范围内的任何字符。
  6. 字符字面值的比较:可以使用等于运算符(==)来比较两个字符常量是否相等。注意,比较时区分大小写,除非使用了比较选项来忽略大小写。
  7. 字符的ASCII码:可以使用十进制数来表示ASCII码。例如:char ch = (char)65; 表示字符A的ASCII码值。
  8. 字符的转换:可以将字符转换为其他数据类型,如字符串或整数。可以使用强制类型转换或使用相应的转换函数。
  9. 使用Character类的方法:System.Char类包含了一系列用于操作和处理字符的方法,例如ToUpper()、ToLower()、IsDigit()等。
  10. 注意事项:在处理字符串时,应小心字符串字面值和字符常量的不同。字符串是由字符组成的序列,而字符常量只表示一个单独的字符。

4.4、字符串常量

字符串常量表示文本数据,通常用于存储和操作文本信息。

  1. 定义方式
    • 使用双引号括起来表示字符串常量,例如:string str = "Hello, World!";
  2. 转义字符
    • 反斜杠\用于转义特殊字符序列,例如:\"表示双引号字符。
  3. 字符串拼接
    • 使用加号+运算符或string.Concat方法来拼接字符串。
  4. 字符串插值
    • 使用符号结合变量进行字符串插值,例如:‘stringname=“Ali**ce”;stringgreeting=“Hello, {name}!”;`。
  5. 字符串长度
    • 使用Length属性获取字符串的长度。
  6. 子串操作
    • 使用Substring方法获取字符串的子串,或使用索引器[]来访问特定位置的字符。
  7. 字符串比较
    • 使用等于运算符(==)来比较两个字符串是否相等,或使用string.Equals方法进行比较。
  8. 字符串格式化
    • 使用格式化字符串或String.Format方法来格式化字符串。
  9. 可变性和不可变性
    • 字符串在C#中是不可变的,意味着一旦创建了字符串,就不能更改其内容。但可以通过拼接、替换或其它操作来创建新的字符串。
  10. 内存管理
  • 由于字符串是不可变的,因此在内存中可能会被共享和重用,以优化性能。

11.编码问题

  • 当处理不同语言的文本时,要注意字符编码问题,如UTF-8、UTF-16等。C#中的字符串是基于Unicode的。

12.注意事项

  • 尽量避免使用老式的String类,而是使用string关键字声明的字符串变量,因为后者提供了更好的类型安全性。
  • 避免使用空引用(null)字符串,因为这会导致空引用异常(NullReferenceException)。在使用之前,应该检查字符串是否为null。
  • 字符串常量在C#中是不可变的,因此当操作字符串时,可能会创建新的字符串实例,这可能会影响性能。因此,对于频繁更改的字符串,应考虑使用其他数据结构(如StringBuilder)。

13.读取和写入文件

  • 使用文件流(如FileStream)和相关的类(如StreamReader和StreamWriter)来读取和写入文件中的字符串数据。

14.编码问题:当从文件或网络读取或写入文本时,需要处理不同的字符编码问题。使用适当的编码来读取和写入数据,以避免乱码或数据损坏。

4.5、定义常量

常量是一种固定值,其值在程序运行期间不能被修改。通过定义常量,可以确保某些值在程序中保持不变,从而提高代码的可读性和维护性。

  1. 关键字:在C#中,使用const关键字来定义常量。
  2. 定义方式
const int MyConstant = 10; // 定义一个整型常量  
const string MyStringConstant = "Hello, World!"; // 定义一个字符串常量
  1. 数据类型:常量可以是任何基本数据类型,如intfloatstring等。
  2. 初始化:在定义常量时必须进行初始化。一旦常量被初始化,其值就不能再被修改。
  3. 作用域:常量的作用域与其声明的位置有关。它们可以是局部变量(在方法内部)、类成员(在类内部)或模块成员(在文件内部)。
  4. 使用场景:常量通常用于表示程序中不会改变的值,例如数学常数、单位转换系数或配置参数等。
  5. 注意事项
    • 常量应该具有描述性的名称,以清晰地表示其用途。
    • 由于常量值在编译时确定并且不可更改,因此不应该使用常量来表示可能会改变的值。
    • 常量应该避免用于表示状态或行为,因为它们是只读的。
  6. readonly关键字比较:虽然constreadonly都用于定义常量值,但它们之间有一些区别。主要区别在于范围和可变性。使用const定义的常量是编译时常量,而使用readonly定义的变量是运行时常量。此外,使用readonly可以在构造器中初始化变量,而const则必须在声明时初始化。
  7. 线程安全:常量和readonly变量是线程安全的,因为它们都是只读的。在多线程环境中,不需要额外的同步措施来访问它们。
  8. 预处理器指令与常量:C#预处理器指令(如#define)可以用于在编译时定义常量。这对于条件编译非常有用,可以根据不同的编译选项设置不同的值。
  9. 平台差异:在不同的平台和框架上,常量的行为可能会有所不同。例如,在P/Invoke(Platform Invocation Services)中使用的平台特定值通常定义为常量。
  10. 与外部库交互:当与外部库交互时,可能需要使用外部库提供的常量。这些常量通常以静态类和静态字段的形式提供。确保在使用外部常量时正确地引用它们,并遵循相关文档和命名约定。

4.6、静态常量和动态常量

4.6.1、 静态常量

定义:静态常量是类级别的常量,它属于类本身而不是类的实例。因此,所有实例共享同一个静态常量的值。

关键字:使用staticconst关键字来定义静态常量。

初始化:静态常量在类首次引用时初始化,且只能赋值一次。

作用域:静态常量可以在类的任何方法中访问,无需创建类的实例。

示例

public class MyClass  
{  
    public static const int StaticConstant = 42;  
    // 其他类成员...  
}

4.6.2、动态常量

定义:动态常量是运行时常量,它的值可以在运行时更改。这与静态常量形成鲜明对比,后者在编译时具有固定的值。

关键字:动态常量没有特定的关键字,通常使用public static和普通的字段(非const)来定义。

初始化与更新:动态常量可以在运行时初始化,并且可以在程序运行期间更新。

示例

public class MyClass  
{  
    public static int DynamicConstant; // 这不是常量,因为它是可变的!  
    // 其他类成员...  
}

4.6.3、 注意事项:

  • 静态常量与编译时常量:静态常量在编译时具有固定的值,这意味着它们的值在编译时确定并不可更改。即使其数据类型为dynamic,其值仍然在编译时确定,并且实际上并不视为真正的动态值。与之相反,动态常量具有动态值,这些值在运行时确定并可能更改。
  • 选择静态常量还是动态常量?:选择静态常量还是动态常量取决于你的需求。静态常量用于表示不会改变的值,而动态常量用于表示可能在运行时更改的值。由于静态常量的值在编译时确定,它们更适合用于表示不会更改的值,如数学常数或配置参数。动态常量更适合用于需要在运行时更改的值,例如计数器或状态信息。
  • 性能考虑:由于静态常量在编译时具有固定值,因此访问它们比访问动态常量更快。这是因为静态常量的值在编译时已经确定,而动态常量的值需要在运行时计算或从外部源获取。因此,对于性能敏感的应用程序,使用静态常量可能更为合适。

五、运算符

运算符是编程语言中的符号,用于执行特定的数学或逻辑运算。C#中内置了许多类型的运算符,可以对数值类型(如int、float、double等)和逻辑类型进行操作。以下是一些C#中常见的运算符:

5.1、算术运算符

算术运算符用于执行基本的数学运算。这些运算符对数值类型(如int、float、double等)进行操作,并返回运算结果。

5.1.1、基本算术运算符

  • 加法(+):将两个数相加。
  • 减法(-):从第一个数中减去第二个数。
  • 乘法(*):将两个数相乘。
  • 除法(/):将第一个数除以第二个数。
  • 取模(%):返回除法的余数。
int a = 5;  
int b = 3;  
int sum = a + b; // 加法运算  
int diff = a - b; // 减法运算  
int product = a * b; // 乘法运算  
int quotient = a / b; // 除法运算,结果为1,因为整数除法舍去小数部分  
int remainder = a % b; // 取模运算,结果为2,因为5除以3的余数是2

5.1.2、增量和减量运算符

  • 增量(++):将变量的值增加1。
  • 减量(–):将变量的值减少1。

5.2、关系运算符

关系运算符用于比较两个值之间的关系。这些运算符返回bool类型的值,表示比较的结果是真(true)还是假(false)。

5.2.1、常见的关系运算符

  • 等于(==):检查两个值是否相等。
  • 不等于(!=):检查两个值是否不相等。
  • 大于(>):检查第一个值是否大于第二个值。
  • 小于(<):检查第一个值是否小于第二个值。
  • 大于或等于(>=):检查第一个值是否大于或等于第二个值。
  • 小于或等于(<=):检查第一个值是否小于或等于第二个值。

5.2.2、操作数类型

关系运算符可以应用于各种数据类型,包括数值类型、字符串等。在进行比较时,C#会根据需要进行类型转换。例如,当比较字符串时,C#会按照字典序比较字符串的字符。

int a = 5;  
int b = 3;  
bool isEqual = a == b; // false,因为5不等于3  
bool isNotEqual = a != b; // true,因为5不等于3  
bool isGreaterThan = a > b; // true,因为5大于3  
bool isLessThan = a < b; // false,因为5不小于3  
bool isGreaterThanOrEqual = a >= b; // true,因为5大于或等于3  
bool isLessThanOrEqual = a <= b; // true,因为5小于或等于3

5.2.3、短路评估

在逻辑表达式中,关系运算符的短路评估行为是很重要的。例如,对于表达式 a && b,如果a为false,则不会评估b,因为即使b是真,整个表达式的结果也必定为false。这种特性在编写条件逻辑时需要特别注意。

5.2.4、与逻辑运算符的结合

关系运算符经常与逻辑运算符一起使用,以构建复杂的条件表达式。例如,可以使用逻辑与(&&)和逻辑或(||)来组合多个关系表达式。

5.2.5、注意事项

  • 在使用关系运算符时,需要注意数据类型的兼容性。例如,不能直接比较不同类型的值,除非它们是可转换的类型。
  • 在比较字符串时,需要注意区分大小写。如果要进行不区分大小写的比较,可以使用String类的Equals方法并传递StringComparison参数。

5.2.6、空值比较

在处理可空类型时,需要注意null值的处理。对于可空类型的值,直接使用关系运算符会导致编译错误。需要使用可空类型特定的比较运算符(如 ==!=),或者使用?.和??运算符来安全地比较空值情况下的值。

5.2.7、自定义类型中的关系运算符

如果需要为自定义类型定义关系运算符的行为,可以通过重载关系运算符来实现。在重载运算符时,需要提供两个操作数和一个返回类型,并定义运算符的行为。自定义的关系运算符可以在自定义类型之间进行比较操作时使用。

5.3、逻辑运算符

逻辑运算符用于组合或修改bool类型的值。这些运算符根据操作数的布尔值返回真(true)或假(false)。

5.3.1、常见的逻辑运算符

  • 逻辑与(&&):当且仅当两个操作数都为true时,结果才为true。
  • 逻辑或(||):当至少一个操作数为true时,结果为true。
  • 逻辑非(!):对操作数的布尔值取反。如果操作数为true,则结果为false;如果操作数为false,则结果为true。
bool a = true;  
bool b = false;  
bool resultAnd = a && b; // false,因为a为true,b为false,所以结果为false  
bool resultOr = a || b; // true,因为至少有一个操作数为true,所以结果为true  
bool resultNot = !a; // false,因为a为true,取反后为false

5.3.2、短路评估

逻辑与(&&)和逻辑或(||)运算符具有短路评估的特性。这意味着在确定结果后,不会评估剩余的操作数。对于逻辑与运算符,如果第一个操作数为false,则不会评估第二个操作数,因为整个表达式已经确定为false。对于逻辑或运算符,如果第一个操作数为true,则不会评估第二个操作数,因为整个表达式已经确定为true。这种短路行为在编写条件语句时非常有用,可以提高性能并避免潜在的错误。

5.3.3、逻辑运算符的优先级

在包含多个逻辑运算符的表达式中,逻辑非(!)具有最高的优先级,其次是逻辑与(&&),最后是逻辑或(||)。因此,在没有使用括号的情况下,表达式会按照优先级顺序进行求值。例如,表达式 a || b && c 将被解释为 a || (b && c)

5.3.4、与关系运算符的结合

逻辑运算符经常与关系运算符一起使用,以构建复杂的条件表达式。关系运算符用于比较值之间的关系,而逻辑运算符用于组合这些比较的结果。例如,可以使用关系运算符检查变量的范围,并使用逻辑运算符将多个条件组合在一起。

5.3.5、注意事项

  • 在使用逻辑运算符时,要确保操作数的类型是bool类型。如果操作数不是bool类型,将会出现编译错误。
  • 需要注意短路评估的行为。在编写条件语句时,确保条件的顺序和组合符合预期的逻辑。

5.3.6、自定义类型中的逻辑运算符

如果需要为自定义类型定义逻辑运算符的行为,可以通过重载逻辑运算符来实现。重载逻辑运算符可以提供自定义的短路评估行为或其他特定的逻辑操作。这允许在自定义类型之间使用逻辑运算符进行条件判断和组合操作。

5.4、位运算符

位运算符用于对整数类型的值进行位级别的操作。这些运算符可以对整数的每一位进行读取、设置、清除和翻转。

5.4.1、常见位运算符

  • 按位与(&):对两个操作数的每一位执行逻辑与操作。
  • 按位或(|):对两个操作数的每一位执行逻辑或操作。
  • 按位异或(^):对两个操作数的每一位执行逻辑异或操作。
  • 按位取反(~):对操作数的每一位执行取反操作。
  • 左移(<<):将操作数的所有位向左移动指定的位数。
  • 右移(>>):将操作数的所有位向右移动指定的位数。
int a = 60; // 60 = 0011 1100  
int b = 13; // 13 = 0000 1101  
int resultAnd = a & b; // 12 = 0000 1100  
int resultOr = a | b; // 61 = 0011 1101  
int resultXor = a ^ b; // 49 = 0011 0001  
int resultNot = ~a; // -61 = 1100 0011  
int resultLeftShift = a << 2; // 240 = 1111 0000  
int resultRightShift = a >> 2; // 15 = 0000 1111

5.4.2、注意事项

  • 位运算符只适用于整数类型,如byte、sbyte、short、ushort、int、long和ulong。
  • 在使用位运算符时,要确保操作数的值在数据类型的范围内,以避免溢出或不确定的行为。
  • 位运算符的行为在处理有符号整数类型(如int和long)时与无符号整数类型(如uint和ulong)有所不同,因为符号位的处理方式不同。

5.4.3、应用场景

位运算符在处理低级编程任务,如硬件通信、网络协议处理、加密算法等场景中非常有用。它们提供了一种直接操作数值内部二进制表示的方法,可以高效地执行某些特定的计算任务。

5.4.4、与其他运算符的结合使用

位运算符可以与其他运算符结合使用,以构建更复杂的表达式。例如,可以结合关系运算符来检查特定位的值,然后使用位运算符进行设置或清除操作。

5.4.5、扩展方法

C#允许通过定义扩展方法来为现有类型添加新的操作符方法。这意味着可以定义自己的位运算符扩展方法,为现有类型提供额外的功能或改变现有运算符的行为。例如,可以定义一个扩展方法来模拟自定义类型的位运算行为。

5.5、赋值运算符

赋值运算符用于将一个值赋给一个变量。

5.5.1、常见赋值运算符

  • 赋值(=):将右侧的值赋给左侧的变量。
  • 加等(+=):将右侧的值加到左侧的变量上,并将结果赋给左侧的变量。
  • 减等(-=):从左侧的变量中减去右侧的值,并将结果赋给左侧的变量。
  • 乘等(*=):将右侧的值乘以左侧的变量,并将结果赋给左侧的变量。
  • 除等(/=):将左侧的变量除以右侧的值,并将结果赋给左侧的变量。
  • 取模(%=):将左侧的变量对右侧的值取模,并将结果赋给左侧的变量。
int a = 5; // 使用赋值运算符将值5赋给a  
a += 3; // 使用加等运算符将3加到a上,并将结果8赋给a  
a -= 2; // 使用减等运算符从a中减去2,并将结果6赋给a  
a *= 4; // 使用乘等运算符将a乘以4,并将结果24赋给a  
a /= 6; // 使用除等运算符将a除以6,并将结果4赋给a  
a %= 5; // 使用取模运算符将a对5取模,并将结果4赋给a

5.5.2、赋值运算符与简单赋值运算符

除了常见的赋值运算符之外,C#还提供了其他一些用于复合赋值的运算符,如加等、减等、乘等、除等和取模。这些复合赋值运算符实际上是简写形式,等同于先执行算术操作,然后再使用赋值运算符。例如,a += 3 等同于 a = a + 3

5.5.3、链式赋值

可以使用赋值运算符将一个表达式的结果连续赋给多个变量。例如:

int a, b, c;  
a = b = c = 10; // 将10赋给a、b和c

5.5.4、注意事项

  • 在使用赋值运算符时,要确保左侧的操作数是一个有效的变量,并且右侧的操作数是可以赋值的。
  • 复合赋值运算符可能会使代码更简洁,但也要注意可读性。在复杂的表达式中使用过多的复合赋值可能会降低代码的可读性。
  • 在进行链式赋值时,要确保每个变量的类型都与右侧的值兼容,并且右侧的值可以被赋给相应的变量。

5.6、运算符的优先级

当一个表达式包含多个运算符时,运算符的优先级决定了它们的执行顺序。优先级高的运算符会先于优先级低的运算符进行计算。

1.一元运算符:
一元运算符对单个操作数执行运算,具有最高优先级。包括:

  • 一元加(+)

  • 一元减(-)

  • 一元正(正值运算符,用于将操作数的值设为正数)

  • 一元负(负值运算符,用于将操作数的值设为负数)
    按位非(对操作数的每一位执行按位非操作)

2.后缀运算符:
后缀运算符在表达式中位于其操作数之后。包括:
postfix ++ 递增运算符(例如,a++)
postfix – 递减运算符(例如,a–)

一元运算符和后缀运算符的优先级高于所有其他运算符

3.算术运算符:

  • 加(+)
  • 减(-)
  • 乘(*)
  • 除(/)
  • 取模(%)
    这些运算符的优先级低于一元和后缀运算符,但高于比较和逻辑运算符。

4.比较运算符:

  • 大于(>)
  • 小于(<)
  • 大于等于(>=)
  • 小于等于(<=)
  • 等于(==)
  • 不等于(!=)
    这些比较运算符具有较低的优先级,位于算术运算符之后。

5.逻辑运算符:

  • 逻辑与(&&)
  • 逻辑或(||)
    这些逻辑运算符的优先级低于比较运算符,但在位运算和赋值运算符之前。

6.位运算符:

  • 按位与(&)
  • 按位或(|)
  • 按位异或(^)
  • 按位取反(~)
  • 左移(<<)
  • 右移(>>)
    这些位运算符的优先级低于逻辑运算和比较运算。

7.赋值运算符:

  • 赋值(=)
  • 加等(+=)
  • 减等(-=)
  • 乘等(*=)
  • 除等(/=)
  • 取模(%=)
    这些赋值运算符具有最低的优先级。它们通常用于链式赋值或组合多个操作。

**8.括号:**括号可以改变表达式的默认执行顺序。任何位于括号内的表达式或子表达式都将首先被计算。例如,在表达式(a + b) * c中,先计算a + b,然后再与c相乘。括号也用于明确地指定计算顺序,即使在没有括号的情况下也是合法的。例如,在表达式a = b * c + d中,首先执行乘法b * c,然后执行加法。但由于赋值运算符的优先级较低,实际上这个表达式的执行顺序与写出的顺序相同。

**9.注意点:**当编写复杂的表达式时,应尽量使用括号明确指定运算顺序,以提高代码的可读性和减少潜在的错误。同时,理解不同类型运算符的优先级有助于编写更精确和高效的代码。

六、条件语句

条件语句在C#中用于根据特定条件执行不同的代码块。这些语句允许程序根据满足的条件作出决策。

6.1、if语句

if语句用于在满足特定条件时执行一段代码。它由一个布尔表达式和一个代码块组成。如果布尔表达式的值为true,则执行代码块。

int number = 10;  
if (number > 5)  
{  
    Console.WriteLine("Number is greater than 5.");  
}`

6.2、if…else语句

if-else语句用于在满足条件时执行一个代码块,否则执行另一个代码块。它包含一个if部分和一个else部分。

int number = 3;  
if (number > 5)  
{  
    Console.WriteLine("Number is greater than 5.");  
}  
else  
{  
    Console.WriteLine("Number is less than or equal to 5.");  
}`

6.3、if嵌套语句

if-else if语句用于检查多个条件,并根据满足的条件执行相应的代码块。它包含一个if部分和一个或多个else if部分。最后,还可以有一个可选的else部分,用于处理所有条件都不满足的情况。

int number = 10;  
if (number > 15)  
{  
    Console.WriteLine("Number is greater than 15.");  
}  
else if (number > 5)  
{  
    Console.WriteLine("Number is greater than 5.");  
}  
else  
{  
    Console.WriteLine("Number is less than or equal to 5.");  
}`

6.4、switch语句

switch语句用于根据一个表达式的值选择不同的执行路径。它包含多个case标签和一个可选的default标签。每个case标签对应一个可能的表达式值,当表达式的值与某个case标签匹配时,执行相应的代码块。如果没有任何case标签与表达式的值匹配,则执行default标签中的代码块(如果有)。

int dayOfWeek = 3;  
switch (dayOfWeek)  
{  
    case 1:  
        Console.WriteLine("Monday");  
        break;  
    case 2:  
        Console.WriteLine("Tuesday");  
        break;  
    case 3:  
        Console.WriteLine("Wednesday");  
        break;  
    // 其他case标签...  
    default:  
        Console.WriteLine("Invalid day");  
        break;  
}//注意:在switch语句的每个case代码块末尾,通常使用break关键字来阻止执行下一个case代码块。这是为了避免执行多个匹配的case代码块。如果不使用break,则所有后续的case代码块都将被执行,直到遇到break或到达switch语句的末尾。这种行为被称为“case穿透”或“fall-through”。但在某些情况下,可以利用这种行为来有意地执行多个连续的case代码块。然而,为了避免意外的错误和混淆,通常建议在每个case代码块的末尾使用明确的break语句。

6.5、条件运算符 ? :

条件运算符(也称为三元运算符)是 ?:。这个运算符允许你在一个表达式中执行一个简单的条件判断,并根据该条件返回两个可能的结果之一。

int a = 5;  
int b = 10;  
  
// 使用条件运算符来判断哪个数字更大,并将结果赋值给max变量  
int max = (a > b) ? a : b;  
Console.WriteLine(max); // 输出 5,因为5不大于10

在这个例子中,由于 a 不大于 b,所以条件为假,因此 max 被赋值为 b

七、循环语句

7.1、循环类型

7.1.1、while循环

while循环在条件为真时重复执行代码块。它只有条件语句,没有初始化或后续语句。

int i = 0;  
while (i < 10)  
{  
    Console.WriteLine(i);  
    i++;  
}

7.1.2、do…while循环

do-while循环至少执行一次代码块,然后根据条件决定是否继续执行。它的条件位于循环的末尾。

int i = 0;  
do  
{  
    Console.WriteLine(i);  
    i++;  
} while (i < 10);

7.1.3、for循环

for循环是最常见的循环类型,它由初始化语句、条件语句和后续语句三个部分组成。

for (int i = 0; i < 10; i++)  
{  
    Console.WriteLine(i);  
}

7.1.4、foreach循环

foreach循环用于遍历集合或数组中的每个元素。它不需要知道集合的大小,也不需要手动控制索引。

int[] numbers = { 1, 2, 3, 4, 5 };  
foreach (int num in numbers)  
{  
    Console.WriteLine(num);  
}

7.2、循环控制语句

7.2.1、break语句

break关键字用于立即退出当前循环,而不管循环条件是否为真。

7.2.2、continue语句

continue关键字用于跳过当前循环的剩余部分,进入下一次迭代。

八、封装

封装是面向对象编程的三大基本特性之一,它能够隐藏对象的内部状态,只通过定义好的接口与外部进行交互。封装有助于提高代码的安全性、可维护性和可重用性。

8.1、定义

  • 封装是将对象的内部状态和行为封装到一个独立的单元中的过程,使得对象的状态不会直接暴露给外部代码。

8.2、主要目的

  • 隐藏对象的实现细节,只暴露必要的接口。
  • 保护对象的内部状态不被外部代码随意修改。
  • 提高代码的安全性和可维护性。

8.3、如何实现

  • 访问修饰符:使用privateprotectedinternalpublic等访问修饰符来控制成员的可见性。
  • 属性与字段:使用属性来控制对字段的访问,而不是直接访问字段。
public class Person  
{  
    private string name; // 私有字段  
    public string Name   
    {  
        get { return name; } // 公共属性,用于读取name  
        set { name = value; } // 公共属性,用于设置name  
    }  
}

8.4、封装的好处

  • 安全性:通过限制外部对内部成员的直接访问,可以防止意外修改或破坏对象的内部状态。
  • 灵活性:通过提供公共接口,允许在不修改内部实现的情况下扩展或修改功能。
  • 代码重用:封装使得代码更易于复用,因为对象的内部实现与外部分离。

8.5、注意事项

  • 不要过度封装,这会增加代码的复杂性并降低性能。
  • 在封装数据的同时,考虑是否需要封装操作。如果某些操作是数据相关的,并且不应该由外部代码执行,则应该将其封装在类中。

8.6、与继承和多态的关系

  • 封装是继承和多态的基础。通过封装,我们可以定义一个基类,并为其成员提供公共接口,然后子类可以继承这些成员并添加更多细节,实现多态。

8.7、实际应用

  • 任何时候当我们想隐藏某些成员并控制对其的访问时,都应该考虑使用封装。例如,控制台应用程序中的类可能包含对用户输入和输出的处理方法,这些方法应该被封装在类中以保护主程序逻辑。
public class Car  
{  
    private string model; // 私有字段  
    public string Model // 公共属性  
    {  
        get { return model; } // 读取model字段的值  
        set { model = value; } // 设置model字段的值  
    }  
      
    private void EngineStart() // 私有方法,表示汽车的启动引擎过程是私有的,不能从外部直接调用  
    {  
        Console.WriteLine("Engine started.");  
    }  
      
    public void StartEngine() // 公共方法,用于启动引擎,这是公共接口的一部分,可以从外部调用  
    {  
        EngineStart(); // 调用私有方法来执行实际操作,遵循封装原则  
    }  
}

九、方法

9.1、概述

在C#编程中,方法是执行特定任务的代码块。它是面向对象编程的基础,允许将相关的代码组织在一起,并通过名称进行调用。方法提供了一种将代码逻辑封装在可重用的单元中的方式,简化了代码的维护和扩展。

9.2、定义

方法是由类或结构体的成员构成的,具有特定功能的一段代码。方法具有名称、返回类型和参数列表。返回类型表示方法返回的数据类型。如果方法不返回任何值,则使用关键字void

9.3、参数

参数是可选的,方法可以没有参数。参数允许向方法传递数据,用于执行特定任务。根据传递方式的不同,参数可以分为值参数(按值传递)、引用参数(按引用传递)和输出参数(用于返回多个值)。

9.4、返回值

方法可以有一个返回值,通过使用return语句返回。如果方法没有返回值,则使用void关键字指定。返回值可以是任何基本数据类型、类或结构体等。

9.5、方法重载

同一个名称可以定义多个方法,只要它们的参数列表不同。这称为方法重载。通过方法重载,可以实现多个功能相似的操作,但具有不同的参数列表。

9.6、特殊类型的方法

  1. 构造函数:用于创建和初始化对象的特殊类型的方法。它们没有返回类型,并且与类名相同。构造函数在创建对象时自动调用,用于初始化对象的属性。
  2. 析构函数:在对象被销毁之前调用的特殊类型的方法。主要用于清理资源。析构函数没有返回类型,也没有参数。它是在对象被垃圾回收器回收之前自动调用的。

9.7、静态方法和实例方法

  1. 静态方法:属于类本身而不是类的实例,可以直接通过类名调用。需要使用static关键字声明。静态方法不能直接访问类的非静态成员和实例成员。
  2. 实例方法:属于类的实例,需要创建类的对象来调用。通常没有使用static关键字声明。实例方法可以直接访问类的非静态成员和实例成员。

9.8、实际应用

在开发过程中,应该根据需要创建适当的方法来组织代码和提高可读性。将相关的代码逻辑组织在方法中可以使代码更易于维护和扩展。通过使用方法重载,可以创建具有相同名称但不同参数的方法,以处理不同的输入参数和实现不同的功能。静态方法和实例方法的区别在于它们的调用方式和使用场景,需要根据实际情况选择使用哪种方法。

class Calculator   
{   
    // 定义一个计算两个数字之和的方法   
    public int Add(int a, int b)   
    {   
        return a + b;   
    }   
}

十、可空类型

10.1、概述

可空类型是一种特殊的类型,用于表示可以为null的值类型。可空类型是C# 8.0引入的新特性,允许值类型拥有null值,增强了代码的灵活性和减少了空引用异常的风险。

10.2、定义

可空类型使用问号(?)后缀来定义。例如,int?表示一个可为null的整数类型。可空类型支持值赋值为null,表示该值未定义或为空。

10.3、应用场景

  1. 表示可以为null的值:对于一些需要表示值可能为空的情况,使用可空类型更加合适。例如,数据库中的记录可能某些字段未定义或为空。
  2. 减少空引用异常:通过允许值类型为null,可以减少对空引用异常的检查,简化代码逻辑。
  3. 更好的类型安全:可空类型提供了更好的类型安全,因为编译器可以检查null值的合法性。

10.4、使用注意事项

  1. 可空类型的默认值是null:对于可空类型,其默认值是null,而不是类型的默认值。例如,int?的默认值是null,而不是0。
  2. 可空类型的运算:可空类型可以进行常规的值类型运算,但需要注意null值的处理。例如,对于int?类型的变量,可以使用加法运算符,但如果变量为null,结果将为null。
  3. 可空类型的比较:比较可空类型时,需要使用可空比较操作符(如?.??)来处理null值的情况。例如,使用?.运算符来安全地访问可空属性或方法。
  4. 可空类型的转换:将可空类型转换为非可空类型时,需要显式检查null值。例如,将int?转换为int时,可以使用条件(三元)运算符(?.)来安全地进行转换。
  5. 可空类型的继承:可空类型可以继承自非可空类型,但非可空类型不能继承自可空类型。

10.5、示例

下面是一个简单的C#可空类型示例:

using System;  
  
class Program  
{  
    static void Main()  
    {  
        int? nullableInt = null; // 可为null的整数类型  
        Console.WriteLine(nullableInt); // 输出null  
        Console.WriteLine(nullableInt.Value); // 抛出NullReferenceException异常,因为变量为null  
        Console.WriteLine(nullableInt ?? 0); // 输出0,因为变量为null时使用0作为默认值  
        Console.WriteLine(nullableInt.GetValueOrDefault()); // 输出0,因为变量为null时使用0作为默认值  
        Console.WriteLine(nullableInt + 1); // 输出1,因为变量为1时进行加法运算  
    }  
}

十一、数组

11.1、概述

数组是用于存储固定大小的同类型元素集合的数据结构。数组可以存储基本数据类型,如int、string等,也可以存储自定义类型的数据。

11.2、创建数组

在C#中,可以通过以下几种方式创建数组:

11.2.1、声明并初始化数组

int[] intArray = new int[5]; // 创建一个包含5个整数的数组  
string[] strArray = new string[] { "Apple", "Banana", "Cherry" }; // 使用花括号初始化数组元素

11.2.2、先声明后初始化

int[] intArray;  
intArray = new int[5]; // 创建并初始化数组

11.2.3、隐式类型数组

var fruits = new string[] { "Apple", "Banana", "Cherry" }; // 编译器能够推断出数组的类型

11.3、访问数组元素

通过索引访问数组中的元素,索引从0开始计数。

int firstNumber = intArray[0]; // 访问第一个元素  
strArray[1] = "Orange"; // 修改第二个元素的值

11.4、多维数组

多维数组包含两个或更多维度。在C#中,可以使用花括号 {} 来创建多维数组。

int[,] twoDArray = new int[3,4]; // 创建一个3行4列的二维数组  
int[,,] threeDArray = new int[2,3,4]; // 创建一个2行3列4高的三维数组

11.5、交错数组(Jagged Arrays)

交错数组是一组不同长度的数组。每个元素都是一个单独的数组。

int[][] jaggedArray = new int[3][]; // 创建一个包含3个整型数组的交错数组  
jaggedArray[0] = new int[5]; // 第一个子数组有5个元素  
jaggedArray[1] = new int[3]; // 第二个子数组有3个元素  
jaggedArray[2] = new int[4]; // 第三个子数组有4个元素

十二、字符串

12.1、概述

字符串是用于表示文本数据的重要数据类型。字符串是由字符组成的序列,用于存储和操作文本数据。

12.2、创建字符串

在C#中,可以使用多种方式创建字符串:

1.直接赋值:

string str = "Hello, World!"; // 创建一个字符串变量并直接赋值

2.使用字符串构造函数:

string str = new string('a', 5); // 使用字符和重复次数创建字符串

3.字符串拼接:

string str = "Hello, " + "World!"; // 使用加号运算符拼接字符串

4.格式化字符串:

string str = string.Format("The answer is {0}", 42); // 使用格式化字符串创建字符串

5.使用StringBuilder类:

StringBuilder sb = new StringBuilder();  
sb.Append("Hello, ");  
sb.Append("World!"); // 使用StringBuilder类构建字符串(适用于多次修改字符串时)

12.3、字符串操作

1.长度获取:使用Length属性获取字符串的长度。

string str = "Hello, World!";  
int length = str.Length; // 获取字符串长度为13

2.索引访问:通过索引访问字符串中的字符。索引从0开始计数。

string str = "Hello, World!";  
char firstChar = str[0]; // 访问第一个字符 'H'

3.子串操作:使用Substring方法获取字符串的子串。

string str = "Hello, World!";  
string subStr = str.Substring(7, 5); // 获取从第7个字符开始的5个字符子串 "World"

4.字符串比较:使用Equals方法或Compare方法比较两个字符串是否相等。

string str1 = "Hello";  
string str2 = "Hello";  
bool isEqual = str1.Equals(str2); // 比较两个字符串是否相等,返回true

5.查找与替换:使用IndexOf或Contains方法查找子串的位置,使用Replace方法替换子串。

string str = "Hello, World!";  
int index = str.IndexOf("World"); // 查找子串的位置,返回值为7  
str = str.Replace("World", "C#"); // 将子串替换为 "C#"

十三、结构体

13.1、概述

结构体(Struct)是用于创建不可变对象的值类型的数据结构。结构体是一种用户定义的数据类型,可以包含多个不同类型的数据成员,并且可以包含方法和属性。

13.2、创建结构体

使用struct关键字来声明一个结构体。结构体的定义通常包括数据成员、构造函数和方法。

public struct Point  
{  
    public int X { get; set; }  
    public int Y { get; set; }  
  
    public Point(int x, int y)  
    {  
        X = x;  
        Y = y;  
    }  
}

在上面的例子中,Point是一个结构体,它有两个整型数据成员XY,以及一个构造函数,用于初始化这些数据成员。

13.3、结构体的特点

  1. 值类型:结构体是值类型,当创建结构体实例时,会分配内存来存储其数据成员的副本。修改结构体实例不会影响其他实例。
  2. 不变性:默认情况下,结构体的字段是只读的,这意味着一旦创建了结构体的实例,就不能修改它的字段。如果需要修改字段,需要将字段标记为可写。
  3. 不可继承:结构体不能被继承,它是不可派生的。
  4. 大小可预测:结构体的布局是固定的,每个字段都有固定的偏移量,因此它的内存大小是可预测的。
  5. 常数表达式初始化:结构体可以使用常数表达式初始化。
  6. 默认构造函数:结构体有一个默认的无参数构造函数。如果需要自定义构造函数,必须手动定义。
  7. 可以定义索引器:结构体可以定义索引器,使其可以像数组一样访问数据。
  8. 可以实现接口:结构体可以实现接口。
  9. 可以嵌套在其他类型中:结构体可以嵌套在其他类型中,例如类或结构体中。
  10. 不能被泛型:不能创建泛型结构体。

13.4、应用场景

  1. 表示简单的数据集合:例如表示点的坐标、表示颜色的RGB值等。
  2. 封装轻量级对象:当需要封装少量数据并快速传递时,使用结构体代替类可以减少内存占用和性能开销。
  3. 用于值类型集合:例如数组、列表或字典等集合中存储值类型时,可以使用结构体作为元素类型。
  4. 用于封装小段数据:例如时间和日期等小段数据可以使用结构体来表示。
  5. 用于简化代码:通过定义结构体可以将相关的数据组合在一起,简化代码的可读性和维护性。

十四、枚举

14.1、概述

枚举(Enum)是用于定义一组命名的整数常量的一种数据类型。枚举提供了一种有效的方式来表示固定数量的选项或状态,使得代码更具可读性和可维护性。

14.2、创建枚举

使用enum关键字来声明一个枚举。枚举的成员默认从0开始递增,也可以显式指定整数值。

public enum Days  
{  
    Sunday,  
    Monday,  
    Tuesday,  
    Wednesday,  
    Thursday,  
    Friday,  
    Saturday  
}

在上面的例子中,Days是一个枚举,它有七个成员,表示一周中的七天。

14.3、枚举的特点

  1. 整数类型:枚举成员实际上是整数值,默认从0开始递增。
  2. 命名常量:枚举成员可以作为命名的整数常量使用,增强了代码的可读性。
  3. 强制转换:可以将枚举值与其他整数类型进行强制转换。
  4. 范围检查:可以方便地检查枚举值是否在指定范围内。
  5. 与整数互操作:可以将枚举值与整数进行算术运算,反之亦然。
  6. 隐式转换:在某些情况下,枚举成员可以隐式转换为整数或字符串。
  7. 可以添加属性:可以为枚举成员添加属性,提供附加的元数据或行为。
  8. 枚举继承:可以为自定义的基类或接口编写派生枚举。
  9. 用于选项集合:可以使用枚举来表示一组选项,例如表示方向(上、下、左、右)或状态(开、关、激活、禁用)。
  10. 常用于标志位操作:使用位运算可以将多个枚举值组合在一起,并在单个变量中进行高效的操作。

14.4、示例应用场景

  1. 表示选项集合:例如定义一周中的几天、颜色选项等。
  2. 状态标记:用于表示不同的状态或条件,例如文件访问模式(只读、写入、追加)或操作结果状态(成功、失败、错误)。
  3. 替代switch语句:当需要使用整数作为分支条件时,可以使用枚举替代switch语句,提高代码的可读性和可维护性。
  4. 配置设置:将配置信息定义为枚举,可以在编译时检查配置的有效性,并方便地进行配置管理。
  5. 位操作标志位:将多个选项组合在一个枚举变量中,通过位运算进行高效的操作和判断。
  • 34
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值