C# 语法知识点
C# 语法知识点#
C# 是微软基于 .NET 平台开发的强类型、面向对象编程语言,广泛应用于桌面应用(WinForms/WPF)、Web 应用(ASP.NET)和游戏开发(Unity)等场景。本文以 WinForms 开发为背景,按基础语法、流程控制、面向对象、常用类库、进阶特性的顺序整理常用语法。
注意:C# 严格区分大小写,Student 与 student 是两个不同的标识符;每条语句以分号 ; 结尾。
1. 程序基本结构#
1.1 命名空间与类#
C# 程序由命名空间(namespace)组织,命名空间下定义类(class),类中包含字段、属性、方法等成员。
using System; // 引入命名空间
namespace StudentManagerPro // 定义命名空间
{
class Program // 定义类
{
static void Main() // 程序入口方法
{
Console.WriteLine("Hello");
}
}
}csharp1.2 注释#
// 单行注释
/* 多行注释
可跨越多行 */
/// <summary>
/// XML 文档注释,描述类和方法的用途,可被 IDE 智能提示读取
/// </summary>csharp2. 变量与数据类型#
2.1 变量与常量#
int age = 20; // 声明并赋值
string name; // 先声明
name = "张三"; // 后赋值
const double PI = 3.14159; // 常量,声明后不可修改csharp2.2 值类型与引用类型#
C# 数据类型分为两大类:
- 值类型(Value Type):变量直接存储数据,赋值时复制副本,互不影响。包括
int、double、bool、char、struct等。 - 引用类型(Reference Type):变量存储对象的引用(地址),赋值时复制引用,两个变量指向同一个对象。包括
string、数组、类(class)等。
int a = 10;
int b = a; // b 是 a 的副本
b = 20; // a 仍为 10
int[] arr1 = { 1, 2, 3 };
int[] arr2 = arr1; // arr2 与 arr1 指向同一个数组
arr2[0] = 100; // arr1[0] 也随之变为 100csharp2.3 常用数据类型#
| 类型 | 说明 | 示例 |
|---|---|---|
int | 32 位整数 | int age = 20; |
long | 64 位整数 | long id = 123456789012L; |
double | 双精度浮点数 | double score = 95.5; |
decimal | 高精度小数,用于金额计算 | decimal price = 9.9M; |
bool | 布尔值 | bool isPassed = true; |
char | 单个字符 | char gender = '男'; |
string | 字符串 | string name = "张三"; |
DateTime | 日期时间 | DateTime birthday = new DateTime(2000, 1, 15); |
注意:string 虽然是引用类型,但具有不可变性(immutable),每次修改都会生成新的字符串对象。
2.4 var 隐式类型#
var 由编译器根据初始值自动推断类型,声明时必须初始化。
var age = 20; // 推断为 int
var name = "张三"; // 推断为 string
var list = new List<int>();csharp注意:var 不是动态类型,编译后类型即固定,之后不能再赋其他类型的值。
2.5 可空类型#
值类型默认不能为 null,在类型后加 ? 可声明可空类型。
int? classId = null;
if (classId.HasValue)
{
int id = classId.Value;
}
// ?? 空合并运算符:左边为 null 时取右边的默认值
int id2 = classId ?? 0;csharp2.6 类型转换#
// 隐式转换:小范围到大范围,自动完成
int a = 10;
double b = a;
// 显式转换(强制转换):大范围到小范围,可能丢失精度
double c = 9.8;
int d = (int)c; // d = 9,小数部分被截断
// Parse:字符串转数值,格式错误会抛出异常
int e = int.Parse("123");
// TryParse:转换失败返回 false,不抛异常(推荐)
if (int.TryParse("123", out int f))
{
// 转换成功,f = 123
}
// Convert:通用转换,null 转数值时返回 0
int g = Convert.ToInt32(reader["Age"]);
// ToString:任意类型转字符串
string s = 123.ToString();csharp2.7 枚举(Enum)#
枚举将一组相关的常量命名,避免在代码中使用难以理解的”魔法值”。
public enum Gender
{
男,
女
}
Gender g = Gender.男;csharp2.8 装箱与拆箱#
装箱(Boxing)是将值类型转换为引用类型(object)的过程,拆箱(Unboxing)则相反。
int a = 10;
object obj = a; // 装箱:值类型 → object
int b = (int)obj; // 拆箱:object → 值类型,必须强制转换为原类型csharp注意:装箱拆箱涉及堆内存分配,频繁发生会影响性能。使用泛型集合(如 List<int>)代替非泛型集合(如 ArrayList)可避免这一问题。
2.9 相等比较(== 与 Equals)#
| 类型 | == 的行为 | Equals() 的行为 |
|---|---|---|
| 值类型 | 比较值 | 比较值 |
string | 比较内容(运算符被重载) | 比较内容 |
| 其他引用类型 | 比较引用(是否同一对象) | 默认比较引用,可重写为比较内容 |
int a = 10, b = 10;
Console.WriteLine(a == b); // True,值比较
string s1 = "abc";
string s2 = "abc";
Console.WriteLine(s1 == s2); // True,string 的 == 比较内容
Student stu1 = new Student();
Student stu2 = stu1; // 指向同一对象
Student stu3 = new Student();
Console.WriteLine(stu1 == stu2); // True
Console.WriteLine(stu1 == stu3); // False,两个不同的对象csharp自定义类可重写 Equals 方法实现按内容比较(如按学号判断两个 Student 是否代表同一人)。
3. 运算符#
| 分类 | 运算符 |
|---|---|
| 算术 | + - * / %(取余) |
| 比较 | == != > >= < <= |
| 逻辑 | &&(并且)||(或者)!(取反) |
| 赋值 | = += -= *= /= |
| 自增自减 | ++ -- |
| 条件(三元) | 条件 ? 值1 : 值2 |
int result = 10 % 3; // 取余,结果为 1
string level = age >= 18 ? "成年" : "未成年"; // 条件运算符csharp注意:&& 和 || 具有短路特性:左边已能确定结果时,右边的表达式不再执行。事件触发前的判空 if (handler != null && handler()) 正是利用了这一特性。
4. 流程控制#
4.1 条件语句(if / switch)#
if (age >= 18)
{
// ...
}
else if (age >= 12)
{
// ...
}
else
{
// ...
}
// switch:按固定值分支
switch (classId)
{
case 1:
Console.WriteLine("软件1班");
break;
case 2:
Console.WriteLine("软件2班");
break;
default:
Console.WriteLine("未知班级");
break;
}csharp注意:C# 的 switch 中每个 case 必须以 break 等跳转语句结束,不能像 JavaScript 那样自动贯穿到下一个 case。
4.2 循环语句#
// for:已知循环次数时使用
for (int i = 0; i < 10; i++) { }
// foreach:遍历集合,日常开发最常用
foreach (Student stu in studentList)
{
Console.WriteLine(stu.StudentName);
}
// while:先判断后执行
while (reader.Read()) { }
// do-while:先执行一次再判断
do { } while (condition);csharp4.3 跳转语句#
break:立即退出当前循环。continue:跳过本次循环的剩余部分,直接进入下一次循环。return:结束当前方法并返回结果。
5. 数组与集合#
5.1 数组(Array)#
数组长度在创建时确定,之后不可改变。
int[] scores = new int[5]; // 长度为 5 的数组
string[] names = { "张三", "李四" }; // 声明同时初始化
int first = scores[0]; // 下标从 0 开始
int len = scores.Length; // 数组长度csharp5.2 List<T>(列表)#
长度可变的泛型集合,T 为元素类型,是日常开发中最常用的集合。
List<Student> list = new List<Student>();
list.Add(new Student()); // 添加元素
list.Remove(stu); // 删除元素
int count = list.Count; // 元素个数
Student first = list[0]; // 按下标访问
bool exist = list.Contains(stu); // 判断是否包含csharp5.3 Dictionary<K,V>(字典)#
键值对(Key-Value)集合,通过键快速查找对应的值。
Dictionary<int, string> classMap = new Dictionary<int, string>();
classMap.Add(1, "软件1班");
string name = classMap[1]; // 按键取值
bool hasKey = classMap.ContainsKey(1); // 判断键是否存在csharp5.4 泛型(Generics)#
泛型允许在定义类、接口、方法时不指定具体类型,改用类型参数 T 占位,使用时再确定具体类型,兼顾代码复用与类型安全。常用的 List<T>、Dictionary<K,V> 中的 T、K、V 都是类型参数。
// 定义泛型类:同一套逻辑适配不同类型
public class Box<T>
{
public T Content { get; set; }
}
// 使用时指定具体类型
Box<int> intBox = new Box<int>() { Content = 10 };
Box<string> strBox = new Box<string>() { Content = "文本" };csharp相比使用 object,泛型无需装箱拆箱与强制类型转换,且类型错误在编译期即可发现。
5.5 LINQ 基础查询#
LINQ(Language Integrated Query)为集合提供类似 SQL 的查询能力,常用方法如下:
| 方法 | 作用 | 类比 SQL |
|---|---|---|
Where(...) | 条件筛选 | where |
Select(...) | 投影,从元素中取出指定字段 | select 列名 |
FirstOrDefault(...) | 取第一个满足条件的元素,无则返回默认值 | top 1 |
Any(...) | 是否存在满足条件的元素 | count(*) > 0 |
OrderBy(...) / OrderByDescending(...) | 升序 / 降序排序 | order by |
Count(...) | 统计满足条件的元素个数 | count(*) |
// 筛选年龄大于 20 的学员
List<Student> result = studentList.Where(s => s.Age > 20).ToList();
// 只取姓名,组成新集合
List<string> names = studentList.Select(s => s.StudentName).ToList();
// 查找学号为 1001 的学员,找不到返回 null
Student stu = studentList.FirstOrDefault(s => s.StudentId == 1001);
// 判断是否存在 2 班的学员
bool exists = studentList.Any(s => s.ClassId == 2);csharp注意:Where、Select 返回的是延迟执行的查询,调用 ToList() 才会立即执行并生成新集合。
6. 字符串处理#
6.1 常用方法#
| 方法 | 作用 | 示例 |
|---|---|---|
Length | 返回长度 | "abc".Length → 3 |
ToUpper() / ToLower() | 大小写转换 | "abc".ToUpper() → “ABC” |
Trim() | 去除两端空格 | " a ".Trim() → “a” |
Substring(起点, 长度) | 截取子串 | "abcdef".Substring(1, 3) → “bcd” |
Replace(旧值, 新值) | 替换 | "aab".Replace("a", "x") → “xxb” |
Contains(s) | 是否包含子串 | "abc".Contains("b") → true |
IndexOf(s) | 子串位置,未找到返回 -1 | "abc".IndexOf("c") → 2 |
Split(分隔符) | 拆分为字符串数组 | "a,b,c".Split(',') |
string.Join(分隔符, 集合) | 将集合拼接为字符串 | string.Join(",", arr) |
string.IsNullOrEmpty(s) | 是否为 null 或空字符串 | 输入校验常用 |
注意:与 SQL 的 substring 不同,C# 中字符串下标从 0 开始。
6.2 字符串格式化#
// string.Format:占位符从 0 开始编号
string s1 = string.Format("姓名:{0},年龄:{1}", name, age);
// $ 字符串插值(C# 6 及以上,推荐):直接在字符串中嵌入变量
string s2 = $"姓名:{name},年龄:{age}";csharp6.3 StringBuilder#
string 不可变,循环内频繁拼接会产生大量临时对象,影响性能。该场景应使用 StringBuilder:
StringBuilder sb = new StringBuilder();
sb.Append("insert into Students ");
sb.Append("values(...)");
string sql = sb.ToString();csharp7. 面向对象#
7.1 类与对象#
类(class)是对象的模板,通过 new 关键字创建对象(实例)。
// 定义类
public class Student
{
// 属性
public int StudentId { get; set; }
public string StudentName { get; set; }
public int Age { get; set; }
// 方法
public string GetInfo()
{
return $"姓名:{StudentName},年龄:{Age}";
}
}
// 创建对象
Student stu = new Student();
stu.StudentName = "张三";
stu.Age = 20;
// 对象初始化器:创建对象的同时为属性赋值
Student stu2 = new Student()
{
StudentName = "李四",
Age = 22
};csharp7.2 属性(Property)#
属性是对字段的封装,通过 get / set 访问器控制读写。
// 自动属性:编译器自动生成背后的私有字段
public string StudentName { get; set; }
// 完整写法:可在访问器中加入校验等逻辑
private int age;
public int Age
{
get { return age; }
set
{
if (value < 0 || value > 150)
{
throw new Exception("年龄不合法");
}
age = value;
}
}
// 受限访问:外部只读,仅类内部可写
public int StudentId { get; private set; }csharp7.3 方法(Method)#
// 返回值类型 方法名(参数列表)
public int Add(int a, int b)
{
return a + b;
}
// void 表示无返回值
public void Show() { }
// 方法重载(Overload):方法名相同、参数列表不同,编译器按实参自动匹配
public bool IsIdNoExistd(string idNo) { }
public bool IsIdNoExistd(string idNo, int studentId) { }
// 可选参数:带默认值的参数,调用时可省略
public void Search(string name = "", int age = 0) { }csharp7.4 参数修饰符(ref / out / params)#
ref:按引用传参,方法内的修改会反映到调用方的变量上,传参前必须先赋值。out:按引用传参,用于让方法带回多个结果,传参前不必赋值,但方法返回前必须为其赋值。params:可变参数,允许传入任意个数的同类型实参,方法内按数组处理。
// ref:交换两个变量的值
public void Swap(ref int a, ref int b)
{
int temp = a; a = b; b = temp;
}
int x = 1, y = 2;
Swap(ref x, ref y); // 调用处也要加 ref,执行后 x = 2,y = 1
// out:一个方法同时带回"是否成功"和"结果"两个值
public bool TryDivide(int a, int b, out int result)
{
if (b == 0)
{
result = 0;
return false;
}
result = a / b;
return true;
}
if (TryDivide(10, 3, out int r))
{
// 除法成功,r = 3
}
// params:接受任意个数的参数
public int Sum(params int[] numbers)
{
int sum = 0;
foreach (int n in numbers) sum += n;
return sum;
}
Sum(1, 2, 3, 4); // 直接传多个值,无需先组装数组csharp前文 int.TryParse("123", out int f) 中的 out 正是利用了这一机制:方法返回值表示转换是否成功,转换结果通过 out 参数带出。
7.5 构造函数(Constructor)#
构造函数在 new 对象时自动执行,用于初始化。方法名与类名相同,没有返回值类型。
public class Student
{
public Student() { } // 无参构造函数
public Student(string name, int age) // 带参构造函数
{
StudentName = name;
Age = age;
}
}
Student stu = new Student("张三", 20);csharp7.6 静态成员(static)#
static 成员属于类本身而非某个对象,通过类名直接访问,无需创建实例。
public class SQLHelper
{
public static string connString = "...";
public static int Update(string sql) { }
}
// 调用方式:类名.成员
SQLHelper.Update(sql);csharp| 对比项 | 实例成员 | 静态成员 |
|---|---|---|
| 归属 | 每个对象各一份 | 全类共享一份 |
| 调用方式 | 对象.成员 | 类名.成员 |
| 内存分配 | 随对象创建而分配 | 程序启动时分配,常驻内存 |
注意:静态方法中不能直接访问实例成员,因为静态方法执行时不存在任何具体对象。
7.7 访问修饰符#
| 修饰符 | 可访问范围 |
|---|---|
public | 任何地方 |
private | 仅当前类内部(类成员的默认级别) |
protected | 当前类及其子类 |
internal | 同一程序集(项目)内 |
7.8 继承(Inheritance)#
子类继承父类的成员,实现代码复用。C# 只支持单继承,即一个类只能有一个直接父类。
// 父类
public class Person
{
public string Name { get; set; }
public void Eat() { }
}
// 子类:冒号表示继承
public class Student : Person
{
public int StudentId { get; set; }
}
Student stu = new Student();
stu.Name = "张三"; // 继承自 Person
stu.Eat(); // 继承自 Personcsharpbase 关键字用于在子类中访问父类成员:
public Student(string name) : base(name) { } // 调用父类构造函数csharp注意:使用 sealed 修饰的类不能被继承,如 sealed class Student。string 就是典型的密封类。
7.9 多态(virtual / override)#
父类方法用 virtual 声明为虚方法,子类用 override 重写。通过父类引用调用该方法时,运行时根据对象的实际类型决定执行哪个版本。
public class Person
{
public virtual void SayHello()
{
Console.WriteLine("我是人");
}
}
public class Student : Person
{
public override void SayHello()
{
Console.WriteLine("我是学生");
}
}
Person p = new Student();
p.SayHello(); // 输出"我是学生",由实际对象类型决定,而非引用类型csharp7.10 抽象类(abstract)#
抽象类不能被实例化,专用于作为其他类的公共基类。用 abstract 修饰的方法没有方法体,子类必须重写实现。
public abstract class Animal
{
public string Name { get; set; }
public void Eat() // 普通方法:包含实现,子类直接继承
{
Console.WriteLine("吃东西");
}
public abstract void MakeSound(); // 抽象方法:只有签名,子类必须实现
}
public class Dog : Animal
{
public override void MakeSound()
{
Console.WriteLine("汪汪");
}
}
// Animal a = new Animal(); // 错误:抽象类不能实例化
Animal dog = new Dog(); // 正确:父类引用指向子类对象csharp注意:包含抽象方法的类必须声明为抽象类;抽象类中也可以只有普通方法而不含抽象方法。
7.11 类型判断(is / as)#
is:判断对象是否为指定类型(或其子类),返回boolas:尝试将对象转换为指定类型,失败时返回null,不抛异常
Person p = new Student();
if (p is Student) // 类型判断
{
Student s = (Student)p; // 强制转换
}
Student s2 = p as Student; // 转换失败时 s2 为 null
if (s2 != null) { }
// C# 7 及以上支持模式匹配,判断与转换一步完成
if (p is Student s3)
{
// 可直接使用 s3
}csharp7.12 接口(Interface)#
接口只定义成员签名,不包含实现。类实现接口后必须实现其全部成员。一个类可以实现多个接口,弥补了单继承的局限。
public interface IStudentService
{
int AddStudent(Student stu); // 只有签名,没有方法体
List<Student> GetStudents();
}
public class StudentService : IStudentService
{
public int AddStudent(Student stu)
{
// 具体实现
}
public List<Student> GetStudents()
{
// 具体实现
}
}csharp接口与抽象类的区别:
| 对比项 | 接口 | 抽象类 |
|---|---|---|
| 方法实现 | 不能包含任何实现 | 可包含已实现的方法 |
| 继承数量 | 一个类可实现多个接口 | 一个类只能继承一个抽象类 |
| 字段 | 不能定义字段 | 可以定义字段 |
8. 委托与事件#
8.1 委托(Delegate)#
委托是方法的类型化引用,用于将方法作为变量传递、存储和调用,相当于前端的回调函数类型。
// 定义委托类型:约定方法的签名(参数与返回值)
public delegate void DataHandler(string data);
// 符合该签名的方法
public void ReceiveData(string data)
{
Console.WriteLine("收到: " + data);
}
// 委托变量指向方法(注意方法名后不加括号,是取引用而非调用)
DataHandler handler = ReceiveData;
handler("张三"); // 调用委托 = 调用 ReceiveData("张三")csharp委托支持多播:通过 += 挂载多个方法,调用时按顺序依次执行。
8.2 事件(Event)#
事件是对委托的封装:外部只能通过 += 订阅、-= 取消订阅,不能直接触发;触发逻辑封装在所属类内部。
public class FrmAddStudent
{
// 声明事件
public static event DataHandler StudentChanged;
// 触发方法:只有所属类内部可以触发事件
public static void NotifyStudentChanged(string name)
{
if (StudentChanged != null) // 触发前必须判空
{
StudentChanged(name);
}
}
}
// 订阅(在其他窗体中)
FrmAddStudent.StudentChanged += OnStudentChanged;
// 取消订阅(窗体关闭时必须执行,防止内存泄漏)
FrmAddStudent.StudentChanged -= OnStudentChanged;csharp8.3 Lambda 表达式#
Lambda 表达式是匿名方法的简写形式,=> 左边是参数,右边是方法体。
// 匿名方法写法
list.Find(delegate(Student s) { return s.Age > 20; });
// Lambda 简写
list.Find(s => s.Age > 20);
// 订阅事件
btnSend.Click += (sender, e) => { MessageBox.Show("点击了"); };csharp9. 异常处理#
9.1 try-catch-finally#
try
{
// 可能出错的代码
int result = Convert.ToInt32(input);
}
catch (FormatException ex)
{
// 捕获特定类型的异常
MessageBox.Show($"格式错误:{ex.Message}");
}
catch (Exception ex)
{
// 捕获所有异常,放在最后
MessageBox.Show($"操作失败:{ex.Message}");
}
finally
{
// 无论是否出错都会执行,通常用于释放资源
conn.Close();
}csharp注意:多个 catch 块按从上到下的顺序匹配,具体异常类型应写在通用 Exception 之前。
9.2 throw 抛出异常#
// 包装原始异常后重新抛出,为上层调用者提供更明确的错误信息
catch (Exception ex)
{
throw new Exception("添加学员失败,数据库操作异常:" + ex.Message);
}csharp10. 资源释放(using 语句)#
对于实现了 IDisposable 接口的资源(数据库连接、文件流等),using 语句可确保使用完毕后自动释放,等价于在 try-finally 中调用 Dispose()。
using (SqlConnection conn = new SqlConnection(connString))
{
conn.Open();
// 使用连接
} // 离开 using 作用域时自动调用 conn.Dispose()csharp11. 常用类库#
除语法本身外,.NET 基类库(BCL)提供了大量现成功能。以下介绍 WinForms 开发中常用的两类。
11.1 DateTime 常用操作#
| 成员 | 作用 |
|---|---|
DateTime.Now | 当前日期时间 |
DateTime.Today | 当前日期(时间部分为 0) |
Year / Month / Day / Hour / Minute / Second | 取年、月、日、时、分、秒 |
AddDays(n) / AddMonths(n) / AddYears(n) | 日期加减 |
ToString("格式") | 按指定格式输出字符串 |
DateTime.Parse(s) / DateTime.TryParse(s, out d) | 字符串转日期 |
DateTime now = DateTime.Now;
string s1 = now.ToString("yyyy-MM-dd"); // 2026-10-17
string s2 = now.ToString("yyyy-MM-dd HH:mm:ss"); // 2026-10-17 18:30:00
DateTime weekLater = now.AddDays(7); // 7 天后
// 计算周岁
int age = DateTime.Now.Year - birthday.Year;csharp注意:DateTime 与 SQL Server 的 datetime 类型对应,写入数据库时直接以 DateTime 对象作为参数传递即可,无需手动转换为字符串。
11.2 文件读写(File)#
System.IO 命名空间下的 File 类提供操作文件的静态方法。
using System.IO;
// 写入文本(覆盖原内容)
File.WriteAllText("log.txt", "第一行内容");
// 追加文本
File.AppendAllText("log.txt", "追加一行");
// 读取全部文本
string content = File.ReadAllText("log.txt");
// 按行读取为数组
string[] lines = File.ReadAllLines("log.txt");
// 判断文件是否存在
bool exists = File.Exists("log.txt");csharp