普通视图

发现新文章,点击刷新页面。
今天 — 2025年11月25日首页

C#从数组到集合的演进与最佳实践

作者 烛阴
2025年11月25日 09:54

一、 数组 (Array)

数组是C#中最基础、最原始的数据集合形式。理解它,是理解所有高级集合的起点。

1. 数组的本质:连续的内存空间

当你声明一个数组,如 int[] numbers = new int[5];,你实际上是在内存中请求了一块连续的、未被分割的空间,其大小足以存放5个int类型的数据。

  • 连续性 (Contiguous):这是数组最重要的特性。数据肩并肩地存储在一起,就像一排有编号的停车位。
  • 固定大小 (Fixed-Size):一旦数组被创建,其大小就不能再改变。你不能让一个长度为5的数组突然容纳第6个元素。
  • 类型统一 (Homogeneous):一个数组只能存储相同类型的元素。

2. 数组的声明与使用

// 1. 声明并初始化大小
int[] scores = new int[5]; // 在内存中分配了5个int的空间,默认值为0

// 2. 通过索引访问 (从0开始)
scores[0] = 95;
scores[1] = 88;
int firstScore = scores[0];

// 3. 声明并直接初始化内容
string[] names = new string[] { "Alice", "Bob", "Charlie" };
// 或者更简洁的语法
string[] namesShort = { "Alice", "Bob", "Charlie" };

// 4. 遍历数组
foreach (string name in names)
{
    Console.WriteLine(name);
}

// 5. 获取长度
int nameCount = names.Length; // 结果是 3

3. 数组的优缺点

优点:

  • 极高的访问性能:由于内存是连续的,通过索引 array[i] 访问元素是一个 O(1) 操作。CPU可以直接通过 基地址 + i * 单个元素大小 的公式计算出内存地址,无需任何遍历。这使得数组在需要频繁随机读取的场景下无与伦比。
  • 内存效率高:除了数据本身,几乎没有额外的开销。

缺点:

  • 大小不可变:这是数组最大的“原罪”。在创建时必须知道所需空间,这在许多动态场景下是不现实的。
  • 插入和删除效率低下:在数组中间插入或删除一个元素,需要移动该位置之后的所有元素来填补空位或腾出空间,这是一个 O(n) 操作,非常耗时。

二、List<T> - 动态数组

1. List<T> 初识

List<T> 的内部其实就封装了一个数组。它之所以能“动态”增长,是因为它实现了一套巧妙的**容量管理(Capacity Management)**机制。

  • Capacity vs. Count:
    • Count: 列表实际包含的元素数量。
    • Capacity: 内部数组能够容纳的元素数量。Capacity >= Count 恒成立。

当你向一个List<T>添加元素时,如果Count即将超过CapacityList<T>会自动执行以下操作:

  1. 创建一个新的、更大的数组(通常是当前容量的两倍)。
  2. 将旧数组中的所有元素复制到新数组中。
  3. 丢弃旧数组,将内部引用指向新数组。
  4. 在新数组的末尾添加新元素。
List<int> numbers = new List<int>(); // 初始Capacity通常为0
Console.WriteLine($"Count: {numbers.Count}, Capacity: {numbers.Capacity}");

numbers.Add(1); // 添加元素
Console.WriteLine($"Count: {numbers.Count}, Capacity: {numbers.Capacity}");  // 创建Capacity为4的容器
numbers.Add(2);
numbers.Add(3);
numbers.Add(4);
numbers.Add(5); // 此时会触发内部数组的扩容和复制!此时Capacity为8

Console.WriteLine($"Count: {numbers.Count}, Capacity: {numbers.Capacity}");

2. List<T> 的使用

List<T> 提供了比数组更加丰富的API方法:

1. 添加和插入元素 (Adding and Inserting Elements)

方法 (Method) 说明 (Description) 示例 (Example) List<string> fruits = new List<string>();
Add(T item) 在列表的末尾添加一个元素。 fruits.Add("Apple"); // ["Apple"]
AddRange(IEnumerable<T> collection) 将一个集合中的所有元素添加到列表的末尾。 var moreFruits = new[] { "Banana", "Cherry" };
fruits.AddRange(moreFruits); // ["Apple", "Banana", "Cherry"]
Insert(int index, T item) 在列表的指定索引处插入一个元素。 fruits.Insert(1, "Orange"); // ["Apple", "Orange", "Banana", "Cherry"]
InsertRange(int index, IEnumerable<T> collection) 在列表的指定索引处插入一个集合的所有元素。 var tropical = new[] { "Mango", "Pineapple" };
fruits.InsertRange(2, tropical);
Contains(T item) 判断列表中是否包含指定的元素。返回 bool bool hasApple = fruits.Contains("Apple"); // true
Exists(Predicate<T> match) 判断列表中是否存在满足指定条件的元素。使用Lambda表达式。 bool hasShortName = fruits.Exists(f => f.Length < 6); // true ("Apple")
Find(Predicate<T> match) 搜索满足指定条件的第一个元素并返回它。如果找不到,返回该类型的默认值(如引用类型为null)。 string bFruit = fruits.Find(f => f.StartsWith("B")); // "Banana"
FindAll(Predicate<T> match) 检索所有满足指定条件的元素,并返回一个包含它们的新 List<T> var longNameFruits = fruits.FindAll(f => f.Length > 5); // ["Orange", "Banana"]
IndexOf(T item) 搜索指定元素,并返回其第一次出现的索引。如果找不到,返回 -1 int index = fruits.IndexOf("Banana"); // 2
LastIndexOf(T item) 搜索指定元素,并返回其最后一次出现的索引。如果找不到,返回 -1 // If fruits = ["A", "B", "A"], LastIndexOf("A") is 2
Remove(T item) 从列表中移除第一次出现的指定元素。成功移除返回 true fruits.Remove("Apple"); // ["Orange", "Banana", "Apple"]
RemoveAt(int index) 移除列表中指定索引处的元素。 fruits.RemoveAt(1); // ["Apple", "Banana", "Apple"]
RemoveAll(Predicate<T> match) 移除所有满足指定条件的元素。返回被移除的元素数量。 int removedCount = fruits.RemoveAll(f => f == "Apple"); // ["Orange", "Banana"]
RemoveRange(int index, int count) 从指定索引开始,移除指定数量的元素。 fruits.RemoveRange(1, 2); // ["Apple", "Apple"]
Clear() 从列表中移除所有元素。Count 变为 0。 fruits.Clear(); // []
Sort() 使用默认比较器对列表中的元素进行就地排序(In-place sort)。 fruits.Sort(); // ["Apple", "Banana", "Cherry"]
Sort(Comparison<T> comparison) 使用指定的委托(通常是Lambda)对元素进行就地排序 fruits.Sort((a, b) => a.Length.CompareTo(b.Length)); // Sort by length
Reverse() 将列表中的元素顺序进行就地反转 fruits.Reverse(); // ["Banana", "Apple", "Cherry"]
ForEach(Action<T> action) 对列表中的每个元素执行指定的操作。 fruits.ForEach(f => Console.WriteLine(f.ToUpper()));
ToArray() 将列表中的元素复制到一个新的数组中。 string[] fruitArray = fruits.ToArray();
GetRange(int index, int count) 创建一个新列表,其中包含源列表中从指定索引开始的指定数量的元素。 var subList = fruits.GetRange(0, 1); // New List containing ["Apple"]

三、Dictionary等特殊集合

1. Dictionary<TKey, TValue> -- 字典

当你需要通过一个唯一的“键”(Key)来快速查找一个“值”(Value)时,Dictionary是你的首选。

  • 本质:基于哈希表(Hash Table)实现。它通过一个哈希函数将Key转换成一个索引,从而实现近乎O(1)的查找、插入和删除性能。
  • 核心特性:Key必须是唯一的,且不可为null
Dictionary<string, int> studentAges = new Dictionary<string, int>();

// 添加键值对
studentAges.Add("Alice", 20);
studentAges["Bob"] = 22; // 更方便的索引器语法

// 查找
if (studentAges.TryGetValue("Alice", out int age))
{
    Console.WriteLine($"Alice's age is {age}"); // 推荐用法,避免异常
}

// 遍历
foreach (var pair in studentAges)
{
    Console.WriteLine($"Key: {pair.Key}, Value: {pair.Value}");
}

2. HashSet<T> - 唯一数组

当你只关心一个元素是否存在于集合中,并且需要保证集合中没有重复元素时,HashSet<T>是完美的选择。

  • 本质:同样基于哈希表,但只存储Key(元素本身),没有Value。
  • 核心特性:元素唯一,查找效率极高(O(1))。支持高效的集合运算(交集、并集、差集)。
HashSet<string> uniqueNames = new HashSet<string>();
uniqueNames.Add("Alice");
uniqueNames.Add("Bob");
uniqueNames.Add("Alice"); // 添加失败,因为 "Alice" 已存在

Console.WriteLine(uniqueNames.Count); // 输出: 2

bool hasBob = uniqueNames.Contains("Bob"); // 极快

3. Queue<T>Stack<T> - 队列和堆栈

  • Queue<T> (队列)先进先出 (FIFO - First-In, First-Out)

    • Enqueue(): 入队(添加到队尾)。
    • Dequeue(): 出队(从队首移除并返回)。
  • Stack<T> (栈)后进先出 (LIFO - Last-In, First-Out)

    • Push(): 入栈(添加到栈顶)。
    • Pop(): 出栈(从栈顶移除并返回)。

结语

点个赞,关注我获取更多实用 C# 技术干货!如果觉得有用,记得收藏本文

昨天 — 2025年11月24日首页
昨天以前首页

C#运算符与表达式终极指南:从入门到精通的万字长文

作者 烛阴
2025年11月20日 09:55

一、什么是运算符与表达式?

  • 运算符(Operator):是一种特殊的符号,用于执行特定的数学、逻辑或位运算。例如 + 是加法运算符,== 是相等运算符。
  • 操作数(Operand):是参与运算的数据。例如,在 5 + 3 中,53 就是操作数。
  • 表达式(Expression):是由操作数和运算符组成的序列,其计算结果会产生一个新值。例如 5 + 3 是一个表达式,它的计算结果是 8x > y 也是一个表达式,它的计算结果是 truefalse

表达式是C#程序的基本执行单元。 理解它们,就是理解程序如何思考。


二、基础运算符

1. 算术运算符

它们负责处理基本的数学计算,与你在小学数学课上学到的几乎一样。

运算符 名称 示例 结果
+ 加法 10 + 5 15
- 减法 10 - 5 5
* 乘法 10 * 5 50
/ 除法 10 / 5 2
% 取模 (求余数) 10 % 3 1
int a = 10;
int b = 3;

Console.WriteLine($"加法: {a + b}");       // 输出: 13
Console.WriteLine($"减法: {a - b}");       // 输出: 7
Console.WriteLine($"乘法: {a * b}");       // 输出: 30
Console.WriteLine($"除法: {a / b}");       // 输出: 3 (注意:整数除法)
Console.WriteLine($"取模: {a % b}");       // 输出: 1

注意事项:整数除法

当两个整数相除时,结果也是一个整数,小数部分会被直接截断(不是四舍五入)。例如 10 / 3 的结果是 3。如果你想得到精确的小数结果,至少要有一个操作数是浮点类型。

double result = 10.0 / 3; // 结果是 3.333...
double result2 = (double)10 / 3; // 结果也是 3.333...

2. 赋值运算符

赋值运算符用于给变量分配一个值。最基本的是 =,但C#提供了一系列复合赋值运算符,让代码更简洁。

运算符 示例 等价于
= x = 5 x = 5
+= x += 5 x = x + 5
-= x -= 5 x = x - 5
*= x *= 5 x = x * 5
/= x /= 5 x = x / 5
%= x %= 5 x = x % 5
int score = 100;
score += 10; // score 现在是 110
score -= 20; // score 现在是 90
score *= 2;  // score 现在是 180
score /= 3;  // score 现在是 60

使用复合赋值运算符不仅代码更短,而且可读性更好,是专业代码的标志之一。

3. 一元运算符

这些运算符只需要一个操作数。

运算符 名称 示例 描述
+ 正号 +5 表示一个正数 (通常省略)
- 负号 -5 表示一个负数 (取反)
++ 递增 x++++x 将变量的值加1
-- 递减 x----x 将变量的值减1

++-- 有两种形式:前缀后缀,它们的区别在于表达式求值的时机

  • 前缀 (Prefix) ++x: 先将 x 的值加1,然后返回加1后的值。
  • 后缀 (Postfix) x++: 先返回 x原始值,然后再将 x 的值加1。
int i = 5;
int j = 5;

// 前缀:先自增,再赋值
int prefixResult = ++i; // i 变成 6, prefixResult 也被赋值为 6
Console.WriteLine($"i: {i}, prefixResult: {prefixResult}"); // 输出: i: 6, prefixResult: 6

// 后缀:先赋值,再自增
int postfixResult = j++; // postfixResult 被赋值为 5, 然后 j 变成 6
Console.WriteLine($"j: {j}, postfixResult: {postfixResult}"); // 输出: j: 6, postfixResult: 5

三、逻辑与比较

1. 关系运算符

也叫比较运算符,用于比较两个操作数,其结果总是一个布尔值 (truefalse)。

运算符 名称 示例 结果为true的条件
== 等于 a == b a 和 b 的值相等
!= 不等于 a != b a 和 b 的值不相等
> 大于 a > b a 的值大于 b
< 小于 a < b a 的值小于 b
>= 大于等于 a >= b a 的值大于或等于 b
<= 小于等于 a <= b a 的值小于或等于 b
int age = 20;
bool isAdult = age >= 18; // isAdult 的值为 true
bool isVoter = age == 21; // isVoter 的值为 false

这些运算符是 if 语句、while 循环等所有控制流语句的核心。

2. 逻辑运算符

用于组合多个布尔表达式,构建更复杂的判断逻辑。

运算符 名称 示例 描述
! 逻辑非 (NOT) !isValid 如果操作数为true,结果为false;反之亦然
&& 逻辑与 (AND) isLogin && isAdmin 两个操作数都为true时,结果才为true
` ` 逻辑或 (OR) `isMember isVIP` 只要有一个操作数为true,结果就为true

短路求值 (Short-circuiting)

&&|| 有一个非常重要的特性叫“短路”。

  • 对于 expr1 && expr2:如果 expr1 的计算结果为 false,那么整个表达式的结果必定是 false,此时 expr2 将不会被计算
  • 对于 expr1 || expr2:如果 expr1 的计算结果为 true,那么整个表达式的结果必定是 true,此时 expr2 将不会被计算

这个特性非常有用,常用于避免空引用异常或减少不必要的计算:

string name = null;

// 如果不使用短路,当 name 为 null 时,name.Length 会抛出 NullReferenceException
// if (name != null & name.Length > 0) { ... }  // 注意这里是 &,非短路

// 使用短路 &&,当 name 为 null 时,第一个条件为 false,第二个条件根本不会执行,非常安全
if (name != null && name.Length > 0)
{
    Console.WriteLine("Name is not empty.");
}

四、位运算符

运算符 名称 描述
& 按位与 两个操作数中,对应位都为1时,结果位才为1
` ` 按位或 两个操作数中,对应位只要有一个为1,结果位就为1
^ 按位异或 两个操作数中,对应位不同时,结果位为1
~ 按位取反 单目运算符,将操作数的所有位反转 (0变1,1变0)
<< 左移 将操作数的所有位向左移动指定的位数,右侧补0
>> 右移 将操作数的所有位向右移动指定的位数

应用场景:权限管理 枚举经常与位运算符结合,用于管理一组开关状态(Flags)。

[Flags]
public enum Permissions
{
    None = 0,       // 0000
    Read = 1,       // 0001
    Write = 2,      // 0010
    Execute = 4,    // 0100
    All = Read | Write | Execute // 0111
}

Permissions userPermissions = Permissions.Read | Permissions.Write;

// 检查是否包含写权限
if ((userPermissions & Permissions.Write) == Permissions.Write)
{
    Console.WriteLine("User has Write permission.");
}

// 添加执行权限
userPermissions |= Permissions.Execute;

// 移除写权限
userPermissions &= ~Permissions.Write;

五、那些强大的“语法糖”

随着C#语言的演进,出现了许多新的运算符,它们极大地简化了代码,使其更具表现力和健壮性。

1. Null 合并运算符 (????=)

用于处理 null 值的利器。

  • ?? (Null-Coalescing Operator): a ?? b 如果 a 不为 null,则表达式的结果是 a;如果 anull,则结果是 b
string userName = null;
string displayName = userName ?? "Guest"; // displayName 的值将是 "Guest"

string userName2 = "Admin";
string displayName2 = userName2 ?? "Guest"; // displayName2 的值将是 "Admin"

这完美地替代了冗长的 if 或三元表达式 (userName != null) ? userName : "Guest"

  • ??= (Null-Coalescing Assignment Operator) (C# 8.0+) variable ??= value 仅当 variablenull 时,才将 value 赋给 variable
List<int> numbers = null;
numbers ??= new List<int>(); // 因为 numbers 是 null,所以为其创建一个新实例

numbers.Add(1);

numbers ??= new List<int>(); // 因为 numbers 不再是 null,所以这条语句什么也不做

这对于延迟初始化(Lazy Initialization)非常有用。

2. Null 条件运算符 (?.?[])

用于优雅地避免 NullReferenceException,告别层层嵌套的 if (obj != null) 检查。

  • ?. (Null-Conditional Member Access): 在访问对象成员(方法或属性)之前,检查对象是否为 null。如果是 null,整个表达式直接返回 null,而不会抛出异常。
string street = "";

User user = null; // GetUser();


// 传统方式,需要层层检查
if (user != null)
{
    if (user.UserAddress != null)
    {
        street = user.UserAddress.Street;
    }
}

// 使用 ?. 运算符,一行搞定!
// 如果 user 或 user.UserAddress 是 null,street 将被赋值为 null
string streetElegant = user?.UserAddress?.Street;


public class User { public Address UserAddress { get; set; } }
public class Address { public string Street { get; set; } }
  • ?[] (Null-Conditional Element Access): 用于访问数组或索引器。
List<string> names = null;
string firstName = names?[0]; // 如果 names 为 null,firstName 为 null,不抛异常

3. 条件运算符 (?:) - 三元表达式

它是 if-else 语句的紧凑形式,适用于简单的条件赋值。

语法: condition ? first_expression : second_expression; 如果 conditiontrue,则计算 first_expression 并将其作为结果;否则计算 second_expression

int age = 20;
string status = (age >= 18) ? "Adult" : "Minor"; // status 的值为 "Adult"

4. 类型相关运算符

运算符 名称 描述
is 类型判断 检查对象是否与给定类型兼容,返回布尔值。C# 7.0+支持模式匹配。
as 类型转换 尝试将对象转换为指定类型,如果转换失败,返回null而不是抛出异常。
typeof 获取类型 返回一个表示类型的 System.Type 对象。
sizeof 获取大小 (仅限非托管类型)返回给定类型值在内存中占用的字节数。
object obj = "Hello World";

if (obj is string)
{
    Console.WriteLine("It's a string.");
}

if (obj is string s) // 如果是string,则直接转换并赋值给 s
{
    Console.WriteLine($"The string has {s.Length} characters.");
}

string str = obj as string; // 转换成功,str 为 "Hello World"
StringBuilder sb = obj as StringBuilder; // 转换失败,sb 为 null

六、运算符的规则 - 优先级与结合性

当一个表达式中包含多个运算符时,谁先计算?这就是**优先级(Precedence)结合性(Associativity)**要解决的问题。

  • 优先级:决定了不同运算符的计算顺序。例如,*/ 的优先级高于 +-,所以 2 + 3 * 4 的结果是 14 而不是 20
  • 结合性:当多个具有相同优先级的运算符在一起时,决定它们的计算方向。
    • 左结合性 (Left-associative):从左到右计算。例如 a - b - c 等价于 (a - b) - c。大多数二元运算符都是左结合的。
    • 右结合性 (Right-associative):从右到左计算。例如赋值运算符 a = b = c 等价于 a = (b = c)。三元运算符也是右结合的。

C#运算符优先级(由高到低摘录)

  1. 主要: x.y, f(x), a[i], x++, x--, new, typeof, sizeof
  2. 一元: +, -, !, ~, ++x, --x, (T)x
  3. 乘法: *, /, %
  4. 加法: +, -
  5. 移位: <<, >>
  6. 关系: <, >, <=, >=, is, as
  7. 相等: ==, !=
  8. 位与: &
  9. 位异或: ^
  10. 位或: |
  11. 逻辑与: &&
  12. 逻辑或: ||
  13. Null合并: ??
  14. 条件: ?:
  15. 赋值与Lambda: =, *=, /=, +=, -=, =>

结语

点个赞,关注我获取更多实用 C# 技术干货!如果觉得有用,记得收藏本文!

C#常量(const)与枚举(enum)使用指南

作者 烛阴
2025年11月19日 09:55

一、 常量 (const)

常量,就是一个其值在程序编译时就已经确定,并且在整个运行期间都不能被修改的量。

如何声明常量?

使用 const 关键字进行声明,并且必须在声明时就进行初始化。

const double PI = 3.14159;
const int DAYS_IN_WEEK = 7;
const string AppVersion = "1.0.2";

命名约定:常量的名称通常使用全大写字母,并用下划线分隔单词,以示区别。

const vs readonly

你可能还会遇到 readonly (只读) 关键字,它与 const 类似但有一个关键区别:

  • const 的值必须在编译时确定。
  • readonly 的值可以在运行时确定(在构造函数中赋值),但一旦赋值后就不能再更改。
public class Config
{
    // 编译时常量
    public const int DefaultTimeout = 5000; 

    // 运行时只读字段
    public readonly DateTime CreationTime;

    public Config()
    {
        // readonly 字段可以在构造函数中初始化
        CreationTime = DateTime.Now; 
    }
}

二、 枚举 (enum)

枚举(enum)允许我们定义一个由一组命名的常量组成的集合,这个集合本身成为一个新的类型。

如何声明和使用枚举?

使用 enum 关键字来定义。

// 定义一个订单状态的枚举
public enum OrderStatus
{
    Pending,    // 待处理
    Processing, // 处理中
    Shipped,    // 已发货
    Completed,  // 已完成
    Cancelled   // 已取消
}

现在,OrderStatus 就成了一个新的数据类型。我们可以这样使用它:

// 声明一个枚举类型的变量
OrderStatus currentStatus = OrderStatus.Pending;

// 在逻辑判断中使用,代码清晰易读
if (currentStatus == OrderStatus.Shipped)
{
    Console.WriteLine("订单已发货!");
}

// 在 switch 语句中使用,是枚举的最佳拍档
switch (currentStatus)
{
    case OrderStatus.Pending:
        // ...
        break;
    case OrderStatus.Processing:
        // ...
        break;
    // ... 其他状态
    default:
        // ...
        break;
}

枚举的优势:

  1. 强类型安全:你不能将一个随意的整数赋给一个枚举变量,编译器会阻止你犯错。这避免了将无效的状态码(比如 orderStatus = 10)赋给变量的可能。
  2. 极佳的可读性OrderStatus.Completed 清晰地表达了其含义,而数字 3 则不能。你的代码会变得“自解释”。
  3. 智能提示(IntelliSense):在Visual Studio等IDE中,当你输入枚举变量时,会自动提示所有可用的成员,极大提高了开发效率并减少了拼写错误。

枚举的底层机制

默认情况下,枚举的每个成员都对应一个整数,从0开始依次递增 (Pending 是 0, Processing 是 1, ...)。你也可以显式地为它们指定值,或者更改其底层的类型(默认为 int)。

public enum ErrorCode : ushort // 可以指定底层类型
{
    None = 0,
    NotFound = 404,
    InternalServerError = 500,
    AccessDenied = 403
}

结语

点个赞,关注我获取更多实用 C# 技术干货!如果觉得有用,记得收藏本文!

❌
❌