前言
这篇文章将原来的 C# 学习笔记(1) 到 (19) 合并为一篇连续阅读稿,方便从 C# 与 .NET Framework 的基本认识一路读到类型系统、面向对象、泛型、LINQ、多线程、异步与文件操作。内容仍按原来的学习顺序展开,但做了统一整理,让整篇更像一份可以顺着读完的阶段性笔记。
C# 与 .NET Framework 起步
什么是 C#
C# 是微软开发的一门面向对象高级程序设计语言,运行在 .NET Framework 提供的执行环境之上。因为背靠 .NET Framework,C# 拥有完整的基础类库、运行时能力以及大量现成控件和开发技术,所以既能写桌面应用,也能写 Web 应用和服务端程序。
C# 的发展历程
理解一门语言,最好先理解它是怎么长大的。C# 从 2000 年发布开始,几乎每个版本都围绕一条明确主线进行演进,比如委托、泛型、Lambda、动态类型和异步编程。
| C# 版本 | .Net Framework 版本 | Visual Studio 版本 | 发布日期 | 代表特性 |
|---|---|---|---|---|
C# 1.0 | .Net Framework 1.0 | Visual Studio .Net 2002 | 2002.1 | 委托、事件 |
C# 1.1 | .Net Framework 1.1 | Visual Studio .Net 2003 | 2003.4 | APM |
C# 2.0 | .Net Framework 2.0 | Visual Studio 2005 | 2005.11 | 泛型、匿名方法、迭代器、可空类型 |
C# 3.0 | .Net Framework 3.0 / 3.5 | Visual Studio 2008 | 2007.11 | var、对象集合初始化器、自动属性、匿名类型、扩展方法、查询表达式、Lambda、表达式树、LINQ |
C# 4.0 | .Net Framework 4.0 | Visual Studio 2010 | 2010.4 | 动态绑定、命名与可选参数、泛型协变与逆变、互操作性 |
C# 5.0 | .Net Framework 4.5 | Visual Studio 2012 | 2012.8 | 异步与等待 |
下图更直观地展示了不同版本的主题演进:

C# 可以做什么
这里只保留最常见的三类场景。
Windows 桌面应用程序
在较早阶段,可以使用 WinForms 开发桌面应用;在后续版本里,也可以使用 WPF 构建更灵活、界面表现更强的桌面程序。

Web 应用程序
.NET Framework 提供了 ASP.NET 技术栈,可以帮助我们快速创建 Web 应用。

Web 服务
在 .Net Framework 3.0 之后,微软提供了 WCF 来实现 Web 服务,它曾是分布式应用开发中的重要方案。

什么是 .NET Framework
很多初学者容易把 C# 和 .NET Framework 混在一起。其实二者关系很简单:
C#是编程语言。.NET Framework是程序运行时执行环境和基础设施。
.NET Framework 提供的核心能力主要包括:
- 全面的类库
- 内存管理
- 通用类型系统
- 丰富的开发技术与框架
- 语言互操作能力
.NET Framework 的组成
公共语言运行时(CLR)
CLR 是 .NET Framework 的核心基础。它可以被理解为托管代码的“运行代理”,负责内存管理、线程调度、异常处理和类型安全校验。受其管理的代码称为托管代码,不受其管理的代码则是非托管代码。
CLR 又包含两个重要部分:
CTS:通用类型系统CLS:公共语言规范
前者帮助不同语言在类型层面达成统一,后者帮助不同语言在规范层面更好协作。
基础类库(BCL / FCL)
.NET Framework 类库本质上是一组程序集的集合,里面定义了大量可复用类型。不同类型会按功能归类到不同命名空间中,例如 System.IO 用于输入输出操作。

C# 代码执行过程
执行过程可以概括为两步:
C#源代码被编译成中间语言(IL)CLR再通过JIT把中间语言编译成本机代码执行
所以,写 C# 时你面对的是高级语言语法,但真正执行时还要依赖 .NET 运行时提供的整套基础设施。
语言基础
标识符与关键字
任何编程语言都离不开“命名”和“保留词”这两件事。
标识符
标识符就是程序员自己起的名字,例如类型名、方法名、变量名、参数名等。下面代码中的 Demo、Program、Main、args、text 都是标识符。
namespace Demo
{
class Program
{
static void Main(string[] args)
{
string text = "Welcome";
Console.WriteLine(text);
Console.ReadKey();
}
}
}
C# 是大小写敏感的,因此 hello 和 Hello 会被认为是两个不同标识符。
关键字
关键字是语言本身保留的单词,例如 namespace、class、void。它们通常不能直接拿来命名,但可以通过在前面加 @ 的方式作为标识符使用,这在字段名与关键字冲突的场景中偶尔会用到。
数据类型
在通用类型系统(CTS)中,类型大体分成两类:值类型和引用类型。
- 值类型:数值类型、布尔类型、枚举类型、结构体类型
- 引用类型:数组、类、接口、委托、
object等
数值类型
数值类型可以继续分成整型、浮点型和十进制型:
- 整型:
int、long、short、byte、char - 浮点型:
float、double - 十进制型:
decimal
其中 decimal 精度高、范围相对小,常用于财务和货币计算。
布尔类型
bool 用于表示逻辑值 true 和 false。
字符串类型
string 是 System.String 的别名,表示由零个或多个字符组成的不可变序列。
class Program
{
static void Main(string[] args)
{
string text = "Welcome";
text = "Hi";
Console.WriteLine(text);
Console.ReadKey();
}
}
这里表面上像是在“修改字符串”,实际上是让变量重新指向了一个新字符串对象。这就是字符串的不可变性。正因为如此,如果频繁拼接和修改字符串,就可能带来额外内存开销。
枚举类型
枚举是一组具名常量,本质上属于值类型。
enum Gender
{
Female,
Male
}
也可以显式指定其底层类型:
enum Gender : byte
{
Female,
Male
}
结构体
结构体是一种值类型,常用于表示轻量级数据对象。
public struct Point
{
public int X;
public int Y;
public Point(int px, int py)
{
X = px;
Y = py;
}
}
数组类型
数组是一种引用类型,表示同类型元素的集合。
变量与常量
变量表示一块可读写的内存空间,常量则在赋值后不能再改变。
变量
变量的基本语法就是:数据类型 变量名。
int a, b, c;
如果局部变量声明后没有初始化就直接使用,编译器会报“使用了未赋值的局部变量”。
变量的作用域
作用域决定变量的生命周期和可访问范围。C# 中常见的是函数作用域和块作用域。
public static void TestMethod()
{
string[] foo = new string[3] { "1", "2", "3" };
foreach (string item in foo)
{
Console.WriteLine(item);
}
// 这里访问不到 item
Console.WriteLine(item);
}
常量
常量用 const 定义:
const int MaxCount = 100;
需要注意:
- 常量必须在声明时初始化
- 常量不能接受变量赋值
- 常量默认是静态的,不能再显式加
static
类和结构体
什么是类
类代表一组具有公共属性和行为的对象。比如“人”这个概念,可以抽象出姓名、年龄等属性,以及说话、行走等行为。
class Person
{
}
通过访问修饰符,可以控制类成员的可见范围:
| 修饰符 | 访问权限 |
|---|---|
private | 仅当前类内部可访问 |
public | 当前程序集及引用程序集都可访问 |
protected | 当前类和派生类可访问 |
internal | 当前程序集内可访问 |
protected internal | 当前程序集或派生类可访问 |
类的常见成员
类中最常见的成员包括字段、属性、方法、构造函数、析构函数和索引器。
字段与属性
字段保存数据,属性用于在封装的基础上对字段进行读写控制。
public class Person
{
private string name;
public string Name
{
get
{
return name;
}
set
{
name = value;
}
}
}
如果字段不希望被随意修改,可以用 readonly 或 const:
public class Person
{
private readonly string name;
public const int DefaultAge = 18;
}
方法
方法由方法签名和方法体组成,用于定义类型的行为。
public class Person
{
public void Speak(string msg)
{
Console.WriteLine(msg);
}
}
构造函数与静态构造函数
实例构造函数用于初始化对象,静态构造函数用于初始化静态成员。
public class Person
{
private static string siteName;
private string name;
static Person()
{
siteName = "Demo";
}
public Person()
{
name = "Carl";
}
}
析构函数
析构函数用于释放对象占用的资源,但现代 C# 开发中更常见的是 IDisposable 模式。
public class Person
{
~Person()
{
Console.WriteLine("析构函数被调用了");
}
}
类与结构体的区别
类和结构体在语法上很像,但本质不同:
- 类用
class定义,结构体用struct定义。 - 类是引用类型,结构体是值类型。
- 类可以继承,结构体不能继承类或结构体。
- 结构体不能定义析构函数。
- 类通常分配在托管堆上,结构体通常按值复制。
public class Point
{
public int X { get; set; }
public int Y { get; set; }
}
public struct Point2
{
public int X { get; set; }
public int Y { get; set; }
}
一般来说,数据量小、语义上更接近“值”的对象适合用结构体;需要继承、共享状态或更复杂行为时更适合用类。
面向对象基础
C# 是面向对象语言,最核心的三个特性是封装、继承和多态。
封装
封装的目的,是隐藏对象内部状态,只暴露安全的访问方式。
public class Person
{
private int _age;
public int Age
{
get
{
return _age;
}
set
{
if (value < 0 || value > 99)
{
throw new Exception("年龄必须在 0 到 99 之间");
}
_age = value;
}
}
}
继承
子类可以继承父类已有的成员,从而减少重复代码。C# 只支持单继承,但允许实现多个接口。
public class Animal
{
public int Age { get; set; }
}
public class Horse : Animal
{
}
public class Sheep : Animal
{
}
当初始化子类时,执行顺序通常是:
- 初始化实例字段
- 调用基类构造函数
- 调用子类构造函数
多态
多态是指相同类型的对象调用相同方法时,表现出不同的行为。常见实现方式是虚方法与重写。
public class Animal
{
public virtual void Voice()
{
Console.WriteLine("动物发出叫声");
}
}
public class Cat : Animal
{
public override void Voice()
{
Console.WriteLine("喵~");
}
}
public class Sheep : Animal
{
public override void Voice()
{
Console.WriteLine("咩~");
}
}
抽象类、密封类与成员隐藏
abstract可以定义抽象类,阻止直接实例化。sealed可以阻止类继续被继承,或阻止虚成员被继续重写。new可以隐藏基类同名成员,但它与override并不一样。
override 是多态,new 是隐藏;两者输出结果可能完全不同。
所有类的根:System.Object
所有类最终都派生自 System.Object。因此,每个对象默认都具备 Equals、GetHashCode、GetType 和 ToString 等基础能力。
接口
接口可以理解为“行为约定”。它声明一组成员,但不提供具体实现,实现接口的类型必须补上这些成员。
interface ICustomCompare
{
int CompareTo(object other);
}
class Person : ICustomCompare
{
public int Age { get; set; }
public int CompareTo(object other)
{
if (other == null)
{
return 1;
}
Person p = (Person)other;
if (Age < p.Age)
{
return -1;
}
if (Age > p.Age)
{
return 1;
}
return 0;
}
}
显式接口实现
当一个类实现多个包含同名成员的接口时,显式接口实现特别有用。
interface IChineseGreeting
{
void SayHello();
}
interface IAmericanGreeting
{
void SayHello();
}
public class Speaker : IChineseGreeting, IAmericanGreeting
{
void IChineseGreeting.SayHello()
{
Console.WriteLine("你好");
}
void IAmericanGreeting.SayHello()
{
Console.WriteLine("Hello");
}
}
这种方式的重点是:接口成员只能通过接口引用访问,而不能直接通过类实例访问。
委托与事件
委托是什么
委托可以看作“类型安全的函数指针”,本质上是一个类,用来封装方法。
public delegate void OutputAction(string msg);
委托的一个重要用途,是把方法当成参数传递。
public class Logger
{
public OutputAction Output;
public Logger()
{
Output = msg => Console.WriteLine(msg);
}
public Logger(OutputAction outputAction)
{
Output = outputAction;
}
public void Debug(string msg)
{
Output(msg);
}
}
多播委托
委托可以通过 + 把多个方法串成调用链,也可以通过 - 移除某个方法。
logger.Output += msg => Console.WriteLine(msg);
logger.Output += msg => Console.WriteLine($"{DateTime.Now}: {msg}");
logger.Output -= myDelegate;
事件是什么
事件是建立在委托基础上的语言特性。event 的核心价值在于:限制外部代码只能订阅和取消订阅,而不能随意触发事件。
public delegate void PublishHandler(string name);
public class AuthorBlog
{
public event PublishHandler Publish;
public void OnPublishing(string name)
{
Console.WriteLine("作者发布了:" + name);
Publish?.Invoke(name);
}
}
EventHandler
实际开发中,更常见的是使用框架提供的 EventHandler 和自定义 EventArgs。
public class BlogAuthorEventArgs : EventArgs
{
public string Name { get; set; }
public BlogAuthorEventArgs(string name)
{
Name = name;
}
}
这种写法更符合 .NET 生态的习惯,也便于统一事件签名。
深入理解类型
值类型与引用类型
C# 类型大体可以分为两类:
- 值类型:如
int、bool、enum、struct - 引用类型:如
class、interface、delegate、string、数组
值类型变量通常直接保存数据本身;引用类型变量保存的是对象引用。
装箱与拆箱
把值类型转换为引用类型,叫装箱;反过来则叫拆箱。
int i = 3;
object o = i; // 装箱
int y = (int)o; // 拆箱
装箱和拆箱都存在额外开销,因此在泛型可以解决问题的场景中,应尽量避免频繁发生这类转换。
参数传递
C# 默认按值传递参数,但“值传递”分成两种常见情况:
- 值类型按值传递:传的是副本
- 引用类型按值传递:传的是引用副本
此外,还可以使用 ref 和 out 实现按引用传递。
private static void ChangeByValue(ref int numValue)
{
numValue = 10;
}
string 虽然是引用类型,但由于不可变性,很多时候它表现得更像“值”。
泛型与 C# 2.0 的核心能力
泛型
泛型的意义,在于把“算法”从“具体类型”中抽离出来,让代码更通用、更安全,也更高效。
public class Compare<T> where T : IComparable<T>
{
public static T Max(T t1, T t2)
{
return t1.CompareTo(t2) > 0 ? t1 : t2;
}
}
泛型的优势主要有三点:
- 减少重复代码
- 避免不必要的装箱和拆箱
- 在编译期保证类型安全
类型参数约束
通过 where 可以限制类型参数的范围:
where T : classwhere T : structwhere T : new()where T : 某个基类或接口
private static T Max<T>(T obj1, T obj2) where T : IComparable<T>
{
return obj1.CompareTo(obj2) > 0 ? obj1 : obj2;
}
可空类型
可空值类型本质上是 Nullable<T> 的语法糖。
int? number = null;
Console.WriteLine(number ?? 2);
?? 空合并运算符可以在左侧为空时返回右侧默认值,常用于简化判空逻辑。
匿名方法与迭代器
匿名方法后来逐渐演化为 Lambda 表达式;迭代器则通过 yield return 极大简化了 IEnumerator 的实现。
public IEnumerator GetEnumerator()
{
for (int i = 0; i < friends.Length; i++)
{
yield return friends[i];
}
}
C# 3.0:让代码更简洁
自动属性与 var
自动属性减少了样板代码,var 则把类型推断交给编译器。
class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
var p = new Person { Name = "Carl", Age = 20 };
对象初始化器、集合初始化器与匿名类型
var list = new List<string> { "Carl", "Karl" };
var anonymous = new { Name = "Carl", Age = 20 };
这些特性让数据创建过程更短、更直观。
Lambda 表达式
Lambda 表达式本质上是更简洁的匿名方法写法。
Func<string, int> getLength = text => text.Length;
它大量用于事件订阅、集合操作和表达式构建。
表达式树
Lambda 表达式不仅可以编译成委托,也可以转为表达式树,以树结构描述代码逻辑。
Expression<Func<int, int, int>> expression = (a, b) => a + b;
Func<int, int, int> del = expression.Compile();
表达式树是后续 LINQ to SQL 等能力的基础之一。
扩展方法
扩展方法让我们可以在不修改原类型的前提下,为现有类型添加“看起来像实例方法”的能力。
public static class IEnumerableExtensions
{
public static bool IsEmpty<T>(this IEnumerable<T> ts)
{
return ts == null || !ts.Any();
}
}
调用时很自然:
List<string> list = new List<string>();
Console.WriteLine(list.IsEmpty());
扩展方法虽然方便,但应尽量扩展具体类型,而不是一股脑扩展 object,否则容易造成 API “污染”。
LINQ
LINQ 是 Language Integrated Query,即语言集成查询。它把查询能力直接带入 C# 语法,使查询集合、XML 甚至数据库时都能保持统一风格。
查询集合
var queryResults = from item in collection
where item % 2 == 0
select item;
与传统 foreach 加 if 相比,LINQ 的表达更集中,也更容易组合多个筛选条件。
查询 XML
XElement xElement = XElement.Parse(xml);
var queryResults = from el in xElement.Elements("Person")
where el.Element("Name")?.Value == "李四"
select el;
LINQ to XML 比手写 XPath 或层层遍历更直观。
LINQ 的本质
对于编译器来说,查询表达式最终仍会被翻译成方法调用,因此理解 Where、Select、OrderBy 等方法的本质很重要。
C# 4.0 与动态能力
可选参数与命名实参
static void Test(int x, int y = 10, string name = "")
{
Console.WriteLine($"{name}:{x},{y}");
}
Test(10, name: "Hello");
这种写法在参数较多时可读性不错,也减少了重载数量。
泛型的协变与逆变
- 协变:更具体的类型可以当作更通用的类型返回,常见于
out - 逆变:更通用的类型可以用于更具体的参数输入,常见于
in
这使得接口和委托在泛型场景中更灵活。
动态类型 dynamic
dynamic 会把类型检查延后到运行时。
dynamic i = 10;
i = i + 10;
它能减少某些强制类型转换,也能与动态语言或运行时对象模型协作,但代价是编译期类型安全下降。
动态行为的实现方式
在 .NET 中,可以通过以下方式实现动态行为:
ExpandoObjectDynamicObjectIDynamicMetaObjectProvider
dynamic d = new ExpandoObject();
d.Name = "测试";
d.AddMethod = (Func<int, int>)(x => x + 1);
Console.WriteLine(d.AddMethod(5));
多线程编程
进程与线程
进程可以理解为资源容器,线程则是其中的执行单元。一个进程至少有一个主线程。
前台线程与后台线程
当前台线程全部结束后,CLR 会直接终止仍在运行的后台线程。
Thread backThread = new Thread(Worker);
backThread.IsBackground = true;
backThread.Start();
如果希望主线程等待后台工作完成,可以使用 Join。
线程池
线程的创建和销毁成本较高,因此 .NET 提供了线程池来复用线程。
ThreadPool.QueueUserWorkItem(CallBackWorkItem);
ThreadPool.QueueUserWorkItem(CallBackWorkItem, "work");
线程池线程默认是后台线程,适合处理短任务。
协作式取消
CancellationTokenSource cts = new CancellationTokenSource();
ThreadPool.QueueUserWorkItem(CallBack, cts.Token);
cts.Cancel();
取消不是“强行掐断”,而是通过 CancellationToken 让任务主动感知并停止。
线程同步
多线程访问共享资源时,需要同步机制来保证数据正确性。
static readonly object globalObj = new object();
lock (globalObj)
{
Console.WriteLine("安全访问共享资源");
}
底层常见同步工具是 Monitor,而 lock 是更简洁的语法糖。线程同步虽然必要,但也会带来复杂度和性能成本,因此应尽量减少共享状态。
异步编程
异步编程适用于下载文件、读取网络资源等耗时任务,目的是避免阻塞主线程,提升用户体验。
同步方式的问题
如果在 UI 线程直接执行耗时操作,界面会卡住,用户无法继续操作。
APM:Begin / End
早期 .NET 使用异步编程模型(APM),常见形式是 BeginXxx / EndXxx。
IAsyncResult result = httpWebRequest.BeginGetResponse(null, null);
httpWebResponse = (HttpWebResponse)httpWebRequest.EndGetResponse(result);
这种模式已经能实现异步,但写法繁琐,而且如果在 UI 线程错误地调用 EndXxx,仍可能把界面堵住。
SynchronizationContext
在桌面应用中,后台线程不能直接操作 UI 控件,通常要借助 SynchronizationContext 把结果切回 UI 线程。
async / await
在 .NET 4.5 之后,async / await 成为更自然的异步写法。
private async Task DownloadFileAsync(string url)
{
HttpWebRequest httpWebRequest = (HttpWebRequest)WebRequest.Create(url);
HttpWebResponse httpWebResponse = (HttpWebResponse)await httpWebRequest.GetResponseAsync();
}
async / await 不一定会新建线程,但它能把“等待”这件事从“阻塞线程”变成“让出线程”,因此在 Web 和 UI 场景里都很重要。
文件操作
文件与目录相关核心类
最常用的文件系统 API 位于 System.IO:
File、FileInfoDirectory、DirectoryInfoStream与各种派生流
File 与 FileInfo
File 更适合一次性静态操作,FileInfo 更适合围绕单个文件对象进行多次操作。
string path = @"C://test.txt";
if (File.Exists(path))
{
File.Delete(path);
}
File.WriteAllText(path, "just for test");
流与读写器
流本质上是一段字节序列的抽象。常见流包括:
FileStreamMemoryStreamNetworkStreamGZipStream
使用 FileStream 可以直接读写字节,也可以配合 StreamReader、StreamWriter 操作文本。
using (FileStream fs = File.Open(path, FileMode.OpenOrCreate))
{
string msg = "test";
byte[] msgArray = Encoding.UTF8.GetBytes(msg);
fs.Write(msgArray, 0, msgArray.Length);
}
异步文件操作
对于大文件写入或 IO 密集场景,可以使用异步文件流。
using (FileStream fs = new FileStream(path, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None, 4096, true))
{
byte[] buffer = Encoding.UTF8.GetBytes("Hello");
await fs.WriteAsync(buffer, 0, buffer.Length);
}
相比旧式的 BeginWrite / EndWrite,现代 C# 中直接使用 WriteAsync 会更自然。
归纳总结
从 C# 学习笔记(1) 到 (19),这一阶段的重点其实可以概括成四件事:
- 建立整体认知:理解
C#与.NET Framework的关系,以及代码真正如何被执行。 - 打好语言基础:掌握标识符、关键字、基础数据类型、变量、常量与作用域。
- 理解类型系统与抽象能力:类、结构体、值类型、引用类型、泛型、接口、委托、事件、扩展方法与 LINQ。
- 具备工程实践意识:线程、异步、文件 IO,以及对性能、封装和可维护性的基本判断。
如果把这些内容串起来看,就会发现它们并不是彼此孤立的知识点,而是 C# 从语言设计到工程开发的一整条主线。前面学到的委托,会在事件和异步里继续出现;前面理解的泛型,会在集合、LINQ 和框架 API 里反复用到;而对值类型、引用类型、线程与 IO 的认识,则会直接影响代码正确性和性能表现。
一句话收尾:这一整组内容学完,写 C# 时就不再只是“会写”,而是开始知道“为什么这么写”。
评论