这段代码是 XAML 中的命名空间声明,用于引入外部程序集(DLL)中的类和控件,以便在 XAML 文件中使用它们。让我们来逐个部分解析:
xmlns:pu
:xmlns
表示 XML 命名空间,pu
是自定义的命名空间前缀。定义了前缀后,可以在 XAML 文件中通过 pu:控件名称
的方式来引用该命名空间中的控件。例如,如果你要使用 Panuon.UI.Silver
库中的某个控件 ButtonX
,可以写为
。
clr-namespace:Panuon.UI.Silver
:这是在 C# 中定义的命名空间名称,Panuon.UI.Silver
是一个包含了自定义控件和样式的命名空间。clr-namespace
表示这是一个“公共语言运行时” (Common Language Runtime) 命名空间,这个部分负责定位到具体的 C# 命名空间。
assembly=Panuon.UI.Silver
:指定了命名空间所属的程序集名称。这个程序集可能是一个外部库(DLL),需要在项目中添加引用后才能使用。
这一行代码的作用是告诉 XAML 文件在哪里找到 Panuon.UI.Silver
命名空间的内容,方便使用该库中的自定义控件或样式。Panuon.UI.Silver
是一个包含 UI 控件和样式的第三方库,通常用于 WPF 应用程序来实现更丰富的 UI 效果。
引入命名空间后,可以直接使用 pu
作为前缀来调用 Panuon.UI.Silver
中的控件:
<pu:ButtonX Content="自定义按钮" />
这个例子中,ButtonX
是 Panuon.UI.Silver
库中的一个自定义按钮控件。
在 WPF(Windows Presentation Foundation)中,通过使用 MVVM(Model-View-ViewModel) 设计模式,可以将 UI(视图)与业务逻辑(视图模型)分离。这使得 XAML 文件中的控件可以绑定到视图模型(ViewModel)中的属性,而不需要这些属性直接存在于代码隐藏文件(例如 PageRecipe1.xaml.cs)中。你的示例正是 MVVM 模式的典型用法。
在 MVVM 模式中:
在你的示例中,Recipes
、SelectedRecipe
和 IsDataGridEnabled
等属性位于 RecipeViewModel.cs
中,而 PageRecipe1.xaml
中的数据绑定将这些属性与视图上的控件进行了连接。
要在视图和视图模型之间建立数据绑定关系,通常会在代码隐藏文件(如 PageRecipe1.xaml.cs)中,将页面的 DataContext
设置为视图模型的实例。这一步确保了 XAML 文件中的数据绑定表达式能够访问视图模型中的属性。
通常在 PageRecipe1.xaml.cs
文件的构造函数中这样设置:
public partial class PageRecipe1 : Page
{
public PageRecipe1()
{
InitializeComponent();
// 创建并设置视图模型
this.DataContext = new RecipeViewModel();
}
}
在这里,DataContext
属性指定了 XAML 中数据绑定的默认数据源。设置了 DataContext
后,视图中的所有绑定都会默认从 RecipeViewModel
中查找对应的属性,比如 Recipes
、SelectedRecipe
和 IsDataGridEnabled
。
因为 DataContext
指定了数据源(即视图模型的实例),所以在 XAML 中可以直接绑定 RecipeViewModel
中的属性。比如:
<DataGrid ItemsSource="{Binding Recipes}" SelectedItem="{Binding SelectedRecipe}" IsEnabled="{Binding IsDataGridEnabled}" />
ItemsSource="{Binding Recipes}"
:在 RecipeViewModel
中查找 Recipes
属性,将其内容绑定到 DataGrid
的 ItemsSource
。SelectedItem="{Binding SelectedRecipe}"
:绑定 DataGrid
的 SelectedItem
到视图模型的 SelectedRecipe
。IsEnabled="{Binding IsDataGridEnabled}"
:绑定 DataGrid
的 IsEnabled
属性到视图模型的 IsDataGridEnabled
。通过这种方式,视图(PageRecipe1.xaml)可以绑定到视图模型(RecipeViewModel)中的属性,而不需要在代码隐藏文件中定义这些属性。这样的结构使得视图和逻辑分离,更加便于管理和测试。
是的,多个 XAML 的后台代码文件(例如多个页面或控件的 .xaml.cs
文件)都可以使用相同的视图模型实例,比如 RecipeViewModel
。这样做在一些场景下是很合理的,尤其是当多个视图需要共享同一组数据或业务逻辑时。不过,也需要注意一些设计上的考虑:
复用逻辑:如果不同视图都需要相同的逻辑或数据(比如一个列表数据和它的操作逻辑),共享一个视图模型可以避免重复代码。
统一状态:共享一个视图模型实例可以使不同视图之间的数据和状态保持同步。例如,如果一个页面更新了 RecipeViewModel
中的数据,其他使用同一视图模型的页面也会反映出相应的更改。
为了更好地管理视图模型实例,通常会通过依赖注入或服务容器来创建和管理视图模型,而不是在每个 XAML 后台文件中都直接实例化它。这样可以确保所有视图使用的是同一个视图模型实例。例如:
public partial class PageRecipe1 : Page
{
public PageRecipe1()
{
InitializeComponent();
this.DataContext = SharedViewModelProvider.GetRecipeViewModel();
}
}
在上例中,SharedViewModelProvider
是一个单独的类,负责提供共享的视图模型实例。通过这种方式,无论在哪个页面中调用 SharedViewModelProvider.GetRecipeViewModel()
,都会得到相同的视图模型实例。
在实际开发中,可以使用依赖注入容器(例如 Microsoft.Extensions.DependencyInjection
)来管理视图模型的生命周期。这样可以轻松地在应用程序中创建并共享视图模型实例。以下是一个简单的示例:
在应用程序启动时配置依赖注入:
public partial class App : Application
{
public IServiceProvider Services { get; }
public App()
{
var services = new ServiceCollection();
services.AddSingleton<RecipeViewModel>(); // 配置为单例
Services = services.BuildServiceProvider();
}
}
在页面中获取共享的视图模型实例:
public partial class PageRecipe1 : Page
{
public PageRecipe1()
{
InitializeComponent();
var recipeViewModel = ((App)Application.Current).Services.GetRequiredService<RecipeViewModel>();
this.DataContext = recipeViewModel;
}
}
通过依赖注入管理视图模型的生命周期,你可以在多个页面中使用相同的视图模型实例,确保数据和状态一致。
多个 XAML 文件确实可以共享同一个视图模型,特别是在数据需要统一和保持一致的场景下。通过依赖注入来管理视图模型实例的创建和生命周期,是一种常见的、方便的实现方法。
这段代码使用了 ThreadPool.QueueUserWorkItem
方法将任务异步地放入线程池中执行。具体解释如下:
ThreadPool.QueueUserWorkItem(new WaitCallback(this.gui_async_thread_callback), new AsyncMotionParamter(operation));
ThreadPool
是 .NET 提供的一个类,负责管理一组线程,用于异步执行任务。当调用 QueueUserWorkItem
方法时,会将任务(即某个方法)加入到线程池队列中,线程池会选择一个空闲线程来执行这个任务。WaitCallback
是 .NET 中的一个委托类型,它表示一个方法,该方法接受一个 Object
类型的参数,并且没有返回值。this.gui_async_thread_callback
表示要在后台线程上执行的方法。gui_async_thread_callback
方法会在后台线程上执行,用来处理需要异步执行的工作。AsyncMotionParamter
类型的对象,并将 operation
作为参数传入。AsyncMotionParamter
是一个自定义类型,封装了要传递给回调方法的数据。operation
),并将在后台线程执行时传递给 gui_async_thread_callback
方法。这段代码将一个任务(gui_async_thread_callback
方法)加入到线程池中异步执行,并传递了包含数据的对象 (AsyncMotionParamter
),这样可以有效避免主线程的阻塞,提升应用的响应性。
线程池队列和线程是两个不同的概念,但它们密切相关,尤其是在处理并发任务时。以下是这两个概念的解释:
线程池是操作系统或编程框架(如 .NET 中的 ThreadPool
类)用来管理一组线程的机制。线程池的主要目的是复用线程资源,提高性能和响应速度。线程池会创建一定数量的线程,并将它们保持在池中,等待执行任务。你不需要手动创建和销毁线程,而是将任务放入线程池队列,由池中的线程来处理。
线程池的关键特点:
线程是操作系统或编程环境分配的最小执行单位,它负责执行代码中的指令。每个线程都有自己的执行路径,可以并发执行任务。线程可以独立地执行任务,或与其他线程协作完成更复杂的任务。
线程的关键特点:
线程池队列是线程池内部管理任务的地方。当你将任务提交给线程池时,任务会先进入队列。线程池中的线程从队列中取出任务并执行。线程池的目的是优化线程的管理和调度,通过复用线程和任务队列来提高系统的并发能力。
线程池的作用是通过复用线程、控制线程数量和管理任务队列,提供更高效的并发处理。
在WPF(Windows Presentation Foundation)中,ObservableCollection
是一个非常重要的集合类,专为数据绑定而设计,能够很好地支持 UI 和数据之间的双向同步。
ObservableCollection
是 .NET 提供的一个动态数据集合类,位于 System.Collections.ObjectModel
命名空间中。它继承自 Collection
,同时实现了 INotifyCollectionChanged
和 INotifyPropertyChanged
接口。
通知机制
ObservableCollection
会自动触发 CollectionChanged
事件。适用于 WPF 数据绑定
ListView
、ComboBox
)通常会通过数据绑定显示集合的数据。ObservableCollection
能够在数据变化时自动刷新 UI。特性 | ObservableCollection | List / Collection |
---|---|---|
通知 UI 更新 | 是 | 否 |
支持动态变化 | 是 | 需要手动通知 UI 更新 |
数据绑定兼容性 | 高 | 需要额外实现接口支持 |
事件支持 | CollectionChanged |
无 |
ObservableCollection
。List
),需要额外手动触发事件(如通过 INotifyPropertyChanged
来更新 UI),增加了开发复杂度。using System.Collections.ObjectModel;
public class MainViewModel
{
public ObservableCollection<string> Items { get; set; }
public MainViewModel()
{
Items = new ObservableCollection<string>
{
"Item 1",
"Item 2",
"Item 3"
};
}
}
绑定到 XAML:
<ListBox ItemsSource="{Binding Items}" />
Items.Add("New Item"); // 添加元素,UI 自动更新
Items.Remove("Item 1"); // 删除元素,UI 自动更新
public class MainViewModel
{
public List<string> Items { get; set; }
public MainViewModel()
{
Items = new List<string>
{
"Item 1",
"Item 2",
"Item 3"
};
}
}
如果你绑定一个 List
到 UI,即使修改了集合内容,UI 也不会更新,因为 List
不会触发更新通知。
ObservableCollection
的理由:List
。通过 ObservableCollection
,你可以大大简化 WPF 应用程序中动态数据绑定的实现过程。
virtual
关键字的用法virtual
关键字用于修饰类成员(方法、属性、索引器或事件),表示这些成员可以在派生类中被 重写(override)。
它是实现 多态性 的关键之一,允许基类定义默认行为,同时提供扩展或更改该行为的能力。
virtual
修饰的成员必须在类中提供实现。override
关键字重写基类的 virtual
成员。sealed
关键字。using System;
public class Animal
{
public virtual void Speak()
{
Console.WriteLine("The animal makes a sound.");
}
}
public class Dog : Animal
{
public override void Speak()
{
Console.WriteLine("The dog barks.");
}
}
public class Cat : Animal
{
public override void Speak()
{
Console.WriteLine("The cat meows.");
}
}
class Program
{
static void Main()
{
Animal animal = new Animal();
Animal dog = new Dog();
Animal cat = new Cat();
animal.Speak(); // Output: The animal makes a sound.
dog.Speak(); // Output: The dog barks.
cat.Speak(); // Output: The cat meows.
}
}
解析:
Animal
类中的 Speak
方法被声明为 virtual
,可以在派生类中被重写。Dog
和 Cat
类通过 override
重写了 Speak
方法,实现了自己的行为。Animal dog
)调用 Speak
时,会执行派生类的实现(多态性)。sealed
关键字防止进一步重写public class Wolf : Dog
{
public sealed override void Speak()
{
Console.WriteLine("The wolf howls.");
}
}
public class ArcticWolf : Wolf
{
// 以下代码会报错,因为 Speak 方法被密封(sealed)。
// public override void Speak()
// {
// Console.WriteLine("The Arctic wolf howls differently.");
// }
}
解析:
Wolf
类中的 Speak
方法被 sealed
修饰,禁止 ArcticWolf
类进一步重写。public class Vehicle
{
public virtual string Type { get; set; } = "Generic Vehicle";
}
public class Car : Vehicle
{
public override string Type { get; set; } = "Car";
}
class Program
{
static void Main()
{
Vehicle myVehicle = new Car();
Console.WriteLine(myVehicle.Type); // Output: Car
}
}
解析:
Type
属性在基类中是虚属性,在派生类中被重写为 Car
的实现。使用 base
关键字调用基类的虚方法。
public class Bird : Animal
{
public override void Speak()
{
base.Speak(); // 调用基类的实现
Console.WriteLine("The bird chirps.");
}
}
class Program
{
static void Main()
{
Animal bird = new Bird();
bird.Speak();
// Output:
// The animal makes a sound.
// The bird chirps.
}
}
解析:
base.Speak()
调用了 Animal
类的默认行为,并在其基础上扩展了功能。virtual
用于私有方法,因为私有成员无法被继承。virtual
关键字为基类提供默认行为,同时允许派生类扩展或替换该行为。override
和 sealed
使用,可以实现灵活的继承体系。在 C# 中,基类的 virtual
方法并不强制要求派生类必须重写。如果派生类未重写这些方法,则会使用基类提供的默认实现。这种机制是为了提供灵活性,使派生类可以选择性地重写基类的方法,而不是必须实现所有的 virtual
方法。
非强制性设计
virtual
方法是一种可选重写机制,而不是强制性接口实现。
如果需要派生类必须实现某些方法,应该使用 接口 (interface
) 或 抽象类 (abstract
)。
示例:使用抽象方法强制实现
public abstract class Base
{
public abstract void MustImplement(); // 抽象方法,派生类必须重写
public virtual void OptionalImplement()
{
Console.WriteLine("Base Implementation");
}
}
public class Derived : Base
{
public override void MustImplement() // 必须重写
{
Console.WriteLine("Implemented");
}
// OptionalImplement 不需要强制重写
}
基类提供默认行为
virtual
方法通常已经提供了默认行为,派生类只有在需要更改或扩展行为时才会重写。派生类未覆盖是合法的
virtual
方法,那么调用该方法时会自动使用基类的实现。如果项目的设计要求 派生类必须覆盖所有的虚方法,可以采取以下方法:
抽象方法强制派生类实现。
public abstract class Base
{
public abstract void Method1();
public abstract void Method2();
}
public class Derived : Base
{
public override void Method1()
{
Console.WriteLine("Method1 Implemented");
}
public override void Method2()
{
Console.WriteLine("Method2 Implemented");
}
}
在开发规范中明确要求派生类应重写所有必要的虚方法。
通过静态代码分析工具(如 Resharper 或 SonarQube)自动检查未重写的方法。
合理使用 virtual
:
virtual
。避免过度重写:
清晰设计接口和类层次:
通过设计和开发规范的配合,可以在灵活性和强制性之间找到平衡。
在 C# 中,static
关键字可以用于修饰类、成员(方法、字段、属性、事件等)以及本地变量,表示该成员或类型属于类本身,而不是某个具体的实例。下面是 static
关键字的用法及其适用范围。
new
运算符创建实例。示例:
public static class MathUtilities
{
public static int Add(int a, int b)
{
return a + b;
}
public static int Multiply(int a, int b)
{
return a * b;
}
}
// 使用
int sum = MathUtilities.Add(3, 5);
示例:
public class Calculator
{
public static int Square(int num)
{
return num * num;
}
}
// 使用
int result = Calculator.Square(4); // 输出: 16
示例:
public class Counter
{
public static int Count = 0;
public Counter()
{
Count++;
}
}
// 使用
Counter c1 = new Counter();
Counter c2 = new Counter();
Console.WriteLine(Counter.Count); // 输出: 2
get
和 set
访问。示例:
public class Configuration
{
private static string _appName = "MyApp";
public static string AppName
{
get { return _appName; }
set { _appName = value; }
}
}
// 使用
Console.WriteLine(Configuration.AppName); // 输出: MyApp
Configuration.AppName = "NewApp";
示例:
public class Logger
{
public static string LogFilePath;
static Logger()
{
LogFilePath = "default.log";
Console.WriteLine("Static constructor called");
}
}
// 使用
Console.WriteLine(Logger.LogFilePath);
示例:
public class OuterClass
{
public static class InnerStaticClass
{
public static void Display()
{
Console.WriteLine("Inner static class");
}
}
}
// 使用
OuterClass.InnerStaticClass.Display();
示例:
void Increment()
{
static int counter = 0; // 静态局部变量
counter++;
Console.WriteLine(counter);
}
// 使用
Increment(); // 输出: 1
Increment(); // 输出: 2
静态关键字用法 | 适用范围 | 特点 |
---|---|---|
静态类 | 类 | 只能包含静态成员,不能实例化 |
静态方法 | 方法 | 属于类本身,不能访问非静态成员 |
静态字段 | 字段 | 属于类本身,所有实例共享 |
静态属性 | 属性 | 封装静态字段,提供全局访问 |
静态构造函数 | 构造函数 | 初始化静态字段,只执行一次 |
静态嵌套类 | 嵌套类 | 只能访问外围类的静态成员 |
静态局部变量 | 方法内部的局部变量(C# 8.0+) | 方法内多次调用间共享状态 |
通过合理使用 static
,可以简化代码设计,提供更高效的全局或共享逻辑。
在 C# 的抽象类中,virtual
和 override
修饰的方法是为派生类提供可选或自定义的实现方式。派生类如何处理这些方法取决于具体修饰符的作用。
virtual
方法定义:
virtual
方法在抽象类中提供了默认实现。override
关键字)。派生类需要做什么?
override
重写此方法。示例:
public abstract class Animal
{
public virtual void Speak()
{
Console.WriteLine("The animal makes a sound.");
}
}
public class Dog : Animal
{
public override void Speak()
{
Console.WriteLine("The dog barks.");
}
}
public class Cat : Animal
{
// 不需要重写,直接使用基类实现
}
调用效果:
Animal animal = new Dog();
animal.Speak(); // 输出: The dog barks.
Animal animal2 = new Cat();
animal2.Speak(); // 输出: The animal makes a sound.
override
方法定义:
override
方法用于重写其 基类(或接口) 的 virtual
或 abstract
方法。派生类需要做什么?
override
重写它。示例:
public abstract class Mammal
{
public virtual void Eat()
{
Console.WriteLine("The mammal is eating.");
}
}
public abstract class Carnivore : Mammal
{
public override void Eat()
{
Console.WriteLine("The carnivore eats meat.");
}
}
public class Lion : Carnivore
{
public override void Eat()
{
Console.WriteLine("The lion hunts and eats.");
}
}
public class Bear : Carnivore
{
// 不重写,继承 Carnivore 的行为
}
调用效果:
Mammal lion = new Lion();
lion.Eat(); // 输出: The lion hunts and eats.
Mammal bear = new Bear();
bear.Eat(); // 输出: The carnivore eats meat.
方法修饰符 | 基类行为 | 派生类行为 |
---|---|---|
virtual |
提供默认实现,派生类可以选择重写 | 可选:不重写则使用基类的实现,重写需用 override |
override |
重写了基类(或接口)的虚方法,提供抽象类自己的实现 | 可选:不重写则继承抽象类的实现,重写需用 override |
abstract |
无实现,必须由派生类实现 | 必须:派生类必须用 override 实现该方法 |
public abstract class Base
{
public virtual void MethodA()
{
Console.WriteLine("Base: MethodA");
}
public abstract void MethodB();
public override string ToString()
{
return "Base Implementation";
}
}
public class Derived : Base
{
public override void MethodA() // 重写 virtual 方法
{
Console.WriteLine("Derived: MethodA");
}
public override void MethodB() // 实现 abstract 方法
{
Console.WriteLine("Derived: MethodB");
}
public override string ToString() // 重写 override 方法
{
return "Derived Implementation";
}
}
class Program
{
static void Main()
{
Base obj = new Derived();
obj.MethodA(); // 输出: Derived: MethodA
obj.MethodB(); // 输出: Derived: MethodB
Console.WriteLine(obj.ToString()); // 输出: Derived Implementation
}
}
对于 virtual
方法:
override
关键字重写。对于 override
方法:
对于 abstract
方法:
Application
类是 WPF 应用程序的核心类之一,它定义了应用程序的生命周期、资源管理和整体行为。它位于命名空间 System.Windows
中,派生自 DispatcherObject
,并且封装了应用程序级的功能。
Application
类的作用管理应用程序的启动和关闭:
Startup
、Exit
和 SessionEnding
,用于处理应用程序的启动和关闭逻辑。全局资源管理:
Resources
属性,用于定义全局共享的资源(比如样式、模板、数据绑定等)。管理主线程:
Application
是 DispatcherObject
的子类,因此它能够与 WPF 的消息循环(Dispatcher)交互,处理主线程的消息。管理窗口:
Windows
集合,用于管理应用程序的所有窗口。MainWindow
。App.xaml
和 App.xaml.cs
的作用App.xaml
:
是 Application
的声明部分,用于定义应用程序的全局资源和启动配置。
默认包含:
<Application x:Class="YourNamespace.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="MainWindow.xaml">
<Application.Resources>
Application.Resources>
Application>
StartupUri
指定启动时要加载的主窗口。App.xaml.cs
:
Application
的代码隐藏文件,通常继承自 Application
类。OnStartup
方法,用于在程序启动时执行初始化逻辑。当 WPF 程序启动时,会按照以下顺序执行:
创建 App
实例:
App
类的构造函数被调用。调用 OnStartup
方法:
默认情况下,OnStartup
方法会引发 Startup
事件。
如果你在 App.xaml.cs
中重写了 OnStartup
方法,那么会从这里开始执行你的自定义逻辑。
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
// 自定义逻辑
}
加载 StartupUri
中指定的窗口:
OnStartup
中手动创建窗口,程序会加载 StartupUri
中指定的窗口。运行消息循环:
Dispatcher
消息循环,应用程序进入运行状态。Application
类是 WPF 应用程序的核心类,负责应用程序生命周期和资源管理。App
类实例,并从重写的 OnStartup
方法开始执行自定义逻辑(如果重写了)。StartupUri
指定的主窗口(除非你在 OnStartup
中做了别的事情,比如手动创建窗口)。下面是对代码 bool.Parse(System.Configuration.ConfigurationSettings.AppSettings["Demo"]);
的详细解释:
System.Configuration.ConfigurationSettings
:
App.config
或 Web.config
)。AppSettings
是其一个静态属性,返回一个键值对集合,存储在配置文件的
节点中。AppSettings["Demo"]
:
节点获取键为 "Demo"
的值,返回的是一个字符串。bool.Parse()
:
bool.Parse
将字符串解析为布尔值(true
或 false
)。"true"
或 "false"
(忽略大小写),会抛出 FormatException
。通常,这段代码需要一个 App.config
或 Web.config
文件,其内容可能如下:
<configuration>
<appSettings>
<add key="Demo" value="true" />
appSettings>
configuration>
Demo
:是键名,表示设置项的名称。true
:是键值,对应代码中读取的值。假设上述配置文件存在并且键 Demo
的值是 "true"
:
读取配置:
System.Configuration.ConfigurationSettings.AppSettings["Demo"]
会从
中获取 "Demo"
对应的值 "true"
(字符串类型)。
解析布尔值:
bool.Parse("true")
会将字符串 "true"
转换为布尔值 true
。
返回结果:
最终,代码返回布尔值 true
。
ConfigurationSettings
已过时:
在 .NET Framework 2.0 及更高版本中,建议使用 ConfigurationManager
类替代 ConfigurationSettings
:
using System.Configuration;
bool result = bool.Parse(ConfigurationManager.AppSettings["Demo"]);
值的有效性:
如果 AppSettings["Demo"]
的值是 null
,bool.Parse
会抛出 ArgumentNullException
。
如果值是非法字符串(如 "yes"
或 "123"
),bool.Parse
会抛出 FormatException
。
为避免异常,建议使用 bool.TryParse
或检查值的有效性:
if (bool.TryParse(ConfigurationManager.AppSettings["Demo"], out bool result))
{
// 使用 result
}
else
{
// 处理解析失败的情况
}
大小写问题:
bool.Parse
是大小写不敏感的,因此 "TRUE"
, "True"
, "false"
等都会正确解析为布尔值。Demo
的键的值,并解析为布尔值。ConfigurationManager
和 bool.TryParse
来提高兼容性和安全性。FrameworkCompatibilityPreferences
类:
System.Windows
命名空间。KeepTextBoxDisplaySynchronizedWithTextProperty
属性:
FrameworkCompatibilityPreferences
的一个静态布尔属性。TextBox
的用户界面(UI)显示是否与其 Text
属性保持同步。true
。在默认情况下(KeepTextBoxDisplaySynchronizedWithTextProperty = true
):
TextBox.Text
属性的值被程序修改时,WPF 会自动更新 TextBox
的 UI 显示。设置为 false
时:
TextBox.Text
的值可以在后台修改,但 UI 不会立即更新,直到某些特定事件触发(如失去焦点)。此选项主要用于提高性能或解决特定场景下的绑定问题。
性能优化:
TextBox.Text
绑定到一个频繁变化的数据源,关闭同步可以减少 UI 更新的次数,从而提高性能。自定义行为:
Text
属性的值分离,以便手动控制显示更新。兼容性调整:
Text
属性)FrameworkCompatibilityPreferences.KeepTextBoxDisplaySynchronizedWithTextProperty = true;
TextBox myTextBox = new TextBox();
myTextBox.Text = "Initial Value";
// 绑定到某个数据源
Binding binding = new Binding("BoundValue");
binding.Source = myDataObject;
myTextBox.SetBinding(TextBox.TextProperty, binding);
// 数据源更新时,UI 的显示也会立即同步更新
myDataObject.BoundValue = "New Value";
// 此时,TextBox 的显示立即变为 "New Value"
FrameworkCompatibilityPreferences.KeepTextBoxDisplaySynchronizedWithTextProperty = false;
TextBox myTextBox = new TextBox();
myTextBox.Text = "Initial Value";
// 绑定到某个数据源
Binding binding = new Binding("BoundValue");
binding.Source = myDataObject;
myTextBox.SetBinding(TextBox.TextProperty, binding);
// 数据源更新时,UI 不会立即同步
myDataObject.BoundValue = "New Value";
// 此时,TextBox 的显示仍然是 "Initial Value"
// 手动触发更新
BindingOperations.GetBindingExpression(myTextBox, TextBox.TextProperty)?.UpdateTarget();
慎用设置为 false
的选项:
手动同步可能增加代码复杂性:
影响范围是全局的:
FrameworkCompatibilityPreferences.KeepTextBoxDisplaySynchronizedWithTextProperty
会影响应用程序中的所有 TextBox
。TextBox
的 UI 显示与其 Text
属性之间的自动同步机制。 AssemblyName assembly = Assembly.GetExecutingAssembly().GetName();
Process processes = Process.GetCurrentProcess();
if (processes.ProcessName != assembly.Name)
{
MessageBox.Show("进程名字已修改,不能启动程序,请把进程名字改回:" + assembly.Name);
System.Environment.Exit(0);
}
System.Diagnostics.Process[] myProcesses = System.Diagnostics.Process.GetProcessesByName(assembly.Name);//获取指定的进程名
if (myProcesses.Length > 1) //如果可以获取到知道的进程名则说明已经启动
{
MessageBox.Show("程序已启动!");
System.Environment.Exit(0);
}
// 确保不存在程序的其他实例
bool createdNew = false;
Semaphore singleInstanceWatcher = new Semaphore(1, 1, assembly.Name, out createdNew);
if (!createdNew)
{
MessageBox.Show("该程序已在运行中!");
System.Environment.Exit(0);
}
这段代码实现了以下功能:
Semaphore
)进一步确保应用程序是单实例运行。下面是详细的解释。
AssemblyName assembly = Assembly.GetExecutingAssembly().GetName();
Process processes = Process.GetCurrentProcess();
Assembly.GetExecutingAssembly()
:获取当前正在执行的程序集。Assembly.GetName()
:获取程序集的名称信息(返回 AssemblyName
对象)。Process.GetCurrentProcess()
:获取当前应用程序的进程信息。if (processes.ProcessName != assembly.Name)
{
MessageBox.Show("进程名字已修改,不能启动程序,请把进程名字改回:" + assembly.Name);
System.Environment.Exit(0);
}
processes.ProcessName
:获取当前进程的名称。assembly.Name
:获取程序集的名称(通常是 .exe
文件名的主体部分)。作用:
System.Diagnostics.Process[] myProcesses = System.Diagnostics.Process.GetProcessesByName(assembly.Name);
if (myProcesses.Length > 1)
{
MessageBox.Show("程序已启动!");
System.Environment.Exit(0);
}
Process.GetProcessesByName(assembly.Name)
:获取所有名称为 assembly.Name
的进程。myProcesses.Length > 1
:
作用:
bool createdNew = false;
Semaphore singleInstanceWatcher = new Semaphore(1, 1, assembly.Name, out createdNew);
if (!createdNew)
{
MessageBox.Show("该程序已在运行中!");
System.Environment.Exit(0);
}
命名信号量的作用:
Semaphore(1, 1, assembly.Name, out createdNew)
:
assembly.Name
)在系统中进行全局唯一标识。createdNew
将返回 false
,表示当前程序实例并未获得锁。createdNew
:标识是否成功创建了新的信号量。如果未创建新的信号量,表示其他实例已经运行,弹出提示并退出程序。
作用:
Semaphore
)进一步确保单实例运行:
异常处理:
代码中未处理可能的异常(如信号量创建失败、权限问题等),建议添加 try-catch
块。
示例:
try
{
Semaphore singleInstanceWatcher = new Semaphore(1, 1, assembly.Name, out bool createdNew);
if (!createdNew)
{
MessageBox.Show("该程序已在运行中!");
System.Environment.Exit(0);
}
}
catch (Exception ex)
{
MessageBox.Show("出现错误:" + ex.Message);
System.Environment.Exit(1);
}
改进用户体验:
Process
类定位已运行实例并将其置于前台。更安全的单实例实现:
Mutex
替代 Semaphore
实现更直观的单实例逻辑。
public class ImageConvert : IValueConverter
{
public ImageConvert() { }
///
/// 转换图片(解决图片被占用问题)
///
///
///
///
///
///
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value == null || string.IsNullOrEmpty(value.ToString()))
return null; // 返回 null 表示无效数据
BitmapImage bitmapImage = null;
try
{
// 确保文件存在
string filePath = value.ToString();
if (!File.Exists(filePath))
return null; // 或返回默认图像
// 读取文件
byte[] bytes;
using (BinaryReader reader = new BinaryReader(File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.Read)))
{
FileInfo fi = new FileInfo(filePath);
bytes = reader.ReadBytes((int)fi.Length);
}
// 加载图像到 BitmapImage
bitmapImage = new BitmapImage();
bitmapImage.BeginInit();
bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
bitmapImage.StreamSource = new MemoryStream(bytes);
bitmapImage.EndInit();
bitmapImage.Freeze(); // 使图像可跨线程访问
}
catch (Exception ex)
{
// 输出异常信息到调试窗口或日志文件
LognetHelper.wrTest($"ImageConvert error: {ex.Message}");
}
return bitmapImage;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return null;
}
}
这段代码定义了一个名为 ImageConvert
的类,实现了 IValueConverter
接口。该接口用于 WPF 数据绑定中的值转换。具体来说,这个转换器用于处理图像加载,尤其是解决图像文件在使用中被占用的问题。
public class ImageConvert : IValueConverter
ImageConvert
类继承并实现了 IValueConverter
接口。该接口在 WPF 中用于数据绑定时将数据转换成目标格式。方法签名:
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
value
:输入的图像路径(通常是字符串)。targetType
:目标绑定类型(通常是 ImageSource
)。parameter
:附加参数(通常未使用)。culture
:区域信息(通常未使用)。BitmapImage
对象,用于 WPF 界面显示。功能概述:
MemoryStream
创建一个新的 BitmapImage
。读取文件:
using (BinaryReader reader = new BinaryReader(File.Open(value.ToString(), FileMode.Open)))
BinaryReader
以二进制模式打开指定路径的图像文件。File.Open
以只读模式打开文件。获取文件信息:
FileInfo fi = new FileInfo(value.ToString());
byte[] bytes = reader.ReadBytes((int)fi.Length);
reader.Close();
BinaryReader
,避免文件被占用。创建 BitmapImage
对象:
bitmapImage = new BitmapImage();
bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
BitmapImage
实例。CacheOption
为 OnLoad
,确保图像数据加载到内存中并释放文件锁定。初始化 BitmapImage
:
bitmapImage.BeginInit();
bitmapImage.StreamSource = new MemoryStream(bytes);
bitmapImage.EndInit();
MemoryStream
) 初始化 BitmapImage
。BeginInit
和 EndInit
包围设置操作,确保完整配置后再加载图像。异常处理:
catch (Exception) { }
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return null;
}
ConvertBack
是 IValueConverter
接口的另一个方法,用于将绑定数据转换回原始类型。在本例中不需要反向转换,因此直接返回 null
。null
或无效路径引发错误。这个错误信息提示你在使用 Visual Studio 2019 时,项目的工具集版本是针对 Visual Studio 2017 的 v141
,但当前的环境中并没有安装该版本的工具。解决这个问题有两个方式:
v141
),你可以安装 Visual Studio 2017 或在当前的 Visual Studio 2019 中单独安装对应版本的工具集。v142
),可以更新项目的目标工具集。v142
。你可以根据自己的需求选择解决方式。
这个错误提示 LNK1181: 无法打开输入文件“Proj_Mpr920.lib”
意味着在链接阶段,编译器找不到 Proj_Mpr920.lib
这个库文件。LNK1181
错误通常是由于以下几种原因引起的:
Additional Library Directories
的设置。Proj_Mpr920.lib
文件的文件夹路径。Proj_Mpr920.lib
文件可能确实缺失,或者路径设置错误,导致编译器无法找到它。Proj_Mpr920.lib
文件存在于你指定的目录下。如果这个库文件应该由某个外部工具或项目生成,确保那个步骤已经正确完成。Proj_Mpr920.lib
,会导致找不到该文件。Proj_Mpr920.lib
。Proj_Mpr920.lib
文件是否存在,并且路径配置正确。如果你能确定这个库文件是由其他项目生成的,请确保这个项目已经构建并生成了 Proj_Mpr920.lib
。
在 WPF 的绑定中,ElementName
和 Source
是用来指定绑定目标的属性来源的,但它们的用途和适用场景有所不同,这就是为什么在你的示例中不能简单地将 ElementName
替换为 Source
的原因。
ElementName
作用:通过元素的名称引用绑定的目标。
场景:用于在同一个 XAML 文件中绑定到另一个元素。
示例解释
:
Text="{Binding Path=Value, ElementName=slider1, UpdateSourceTrigger=PropertyChanged}"
这里
ElementName=slider1
明确指定了绑定目标为
slider1
(通过名字引用这个 Slider 控件)。这个方法适用于同一 XAML 文件中的控件绑定。
Source
作用:直接引用绑定目标对象的实例。
场景:通常在 XAML 或代码中绑定到特定的对象实例,而不是通过名字引用控件。
问题: 如果你想用 Source
替换 ElementName
,需要将目标对象明确为 slider1
的实例,例如:
Text="{Binding Path=Value, Source={x:Reference slider1}, UpdateSourceTrigger=PropertyChanged}"
但默认情况下,XAML 中的控件(如 slider1
)不是直接作为对象实例来使用的,而是通过名称查找的。
如果你在绑定中使用 Source
,那么 slider1
必须是一个已经实例化的对象,而不是一个通过名称引用的控件。
ElementName
更适合绑定到同一 XAML 文件中的控件。Source
更适合绑定到已经实例化的对象。slider1
是一个 XAML 中的控件,用 ElementName
是正确的做法。如果你希望用 Source
,则需要更复杂的设置,通常没有必要这样做。在你的代码中,PropertyChanged
是一个事件,通常会在 XAML 数据绑定过程中被赋值。具体来说,PropertyChanged
事件被赋值的时机是当 UI 控件绑定到 Student
对象并订阅该事件时。
绑定建立时:
Student
对象绑定到 UI 元素(例如 TextBox
的 Text
属性)时,WPF 的数据绑定机制会自动订阅 PropertyChanged
事件。事件订阅:
WPF 数据绑定引擎会自动向 PropertyChanged
添加一个事件处理器,以便在 PropertyChanged
事件被触发时,更新绑定的 UI 控件。
例如,如果绑定如下:
并且 DataContext设置为一个 Student实例:
textBox.DataContext = new Student();
WPF 会为 Student实例的 PropertyChanged事件订阅一个回调函数。
什么时候触发:
Name
属性的值发生变化并调用 PropertyChanged?.Invoke(...)
时,WPF 会收到通知,并根据绑定的定义更新 UI。如果你想验证 PropertyChanged
事件是否被赋值,可以在 set
方法中断点调试,或者输出日志:
set
{
name = value;
if (PropertyChanged != null)
{
Console.WriteLine("PropertyChanged has subscribers.");
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Name"));
}
}
如果绑定已建立且 PropertyChanged
被赋值,你会看到输出说明事件有订阅者。
PropertyChanged
的赋值时机:
PropertyChanged
事件。在 WPF/XAML 中,声明 xmlns:controls="http://www.maxwell.com/controls"
并让 XAML 解析器自动定位到 MwFramework.Controls.UIControl.dll
的行为,是通过 WPF 的 XML 命名空间映射机制实现的。以下是完整的实现原理:
XmlnsDefinitionAttribute
这是 WPF/XAML 的元数据标记,用于将 XML 命名空间 URI(如 http://www.maxwell.com/controls
)映射到 CLR 命名空间(如 MwFramework.Controls
)。
具体步骤如下:
在 MwFramework.Controls.UIControl.dll
的代码中,需要添加如下程序集级特性(通常在 AssemblyInfo.cs
中):
[assembly: XmlnsDefinition("http://www.maxwell.com/controls", "MwFramework.Controls")]
[assembly: XmlnsDefinition("http://www.maxwell.com/controls", "MwFramework.Controls.CameraAxis")] // 可选:映射多个命名空间
当 XAML 解析器遇到 xmlns:controls="http://www.maxwell.com/controls"
时,会:
XmlnsDefinitionAttribute
的特性。MwFramework.Controls.UIControl.dll
中注册的 http://www.maxwell.com/controls
URI。MwFramework.Controls
)。MwFramework.Controls.Non-Touch
包(版本 1.0.1.28
),编译时 MSBuild 会自动将该 DLL 复制到输出目录(如 bin\Debug
)。.csproj
)中的引用定位到该程序集。MwFramework.Controls.UIControl.dll
)。XmlnsDefinitionAttribute
找到已加载程序集中的类型。XmlnsDefinitionAttribute
解耦了 XML 命名空间与物理路径的直接绑定。MwFramework.Controls.UIControl
)在项目中正确引用,XAML 解析器就能通过反射找到类型,无需关心物理路径。若怀疑映射失败,可以通过以下方法调试:
使用反编译工具(如 ILSpy)打开 MwFramework.Controls.UIControl.dll
,查看其是否包含 XmlnsDefinitionAttribute
:
// 反编译结果示例
[assembly: XmlnsDefinition("http://www.maxwell.com/controls", "MwFramework.Controls")]
确保 .csproj
文件中包含对该程序集的引用:
<Reference Include="MwFramework.Controls.UIControl">
<HintPath>..\packages\MwFramework.Controls.Non-Touch.1.0.1.28\lib\net461\MwFramework.Controls.UIControl.dllHintPath>
Reference>
packages.config
或 .csproj
中引用了 MwFramework.Controls.Non-Touch
包:<PackageReference Include="MwFramework.Controls.Non-Touch" Version="1.0.1.28" />
XmlnsDefinitionAttribute
XmlnsDefinitionAttribute
,需手动声明完整的 CLR 命名空间:xmlns:controls="clr-namespace:MwFramework.Controls;assembly=MwFramework.Controls.UIControl"
XmlnsDefinitionAttribute
绑定到程序集中的 CLR 命名空间。\packages\...\MwFramework.Controls.UIControl.dll
)由 NuGet 包管理器和项目引用自动处理,开发者无需手动指定。这种设计使得 XAML 代码与物理路径解耦,提高了代码的可维护性和跨项目复用性。
在面向对象编程(OOP)中,封装、继承和多态被称为类的三大要素。这些特性为程序的模块化、可扩展性和复用性提供了重要的基础。
封装是将数据(属性)和操作数据的方法(行为)绑定在一起,并隐藏对象的内部实现细节,仅对外暴露必要的接口。
private
、protected
、public
等访问修饰符控制外部对类中成员的访问。public class BankAccount
{
private decimal balance; // 私有字段,隐藏实现细节
public void Deposit(decimal amount) // 提供公开方法操作数据
{
if (amount > 0)
{
balance += amount;
}
}
public void Withdraw(decimal amount)
{
if (amount > 0 && amount <= balance)
{
balance -= amount;
}
}
public decimal GetBalance() // 提供只读的访问方式
{
return balance;
}
}
// 调用示例
BankAccount account = new BankAccount();
account.Deposit(100);
account.Withdraw(50);
Console.WriteLine(account.GetBalance()); // 输出 50
继承是通过一个类(子类)从另一个类(父类)继承属性和方法,从而实现代码复用和扩展的机制。
override
关键字)。public class Animal
{
public string Name { get; set; }
public void Eat()
{
Console.WriteLine($"{Name} is eating.");
}
}
public class Dog : Animal
{
public void Bark()
{
Console.WriteLine($"{Name} is barking.");
}
}
// 调用示例
Dog dog = new Dog();
dog.Name = "Buddy";
dog.Eat(); // 输出 "Buddy is eating."
dog.Bark(); // 输出 "Buddy is barking."
多态是指同一个方法在不同对象上可以表现出不同的行为。它允许程序通过父类引用调用子类的方法,从而实现动态绑定。
public class Animal
{
public virtual void Speak()
{
Console.WriteLine("Animal makes a sound.");
}
}
public class Dog : Animal
{
public override void Speak()
{
Console.WriteLine("Dog barks.");
}
}
public class Cat : Animal
{
public override void Speak()
{
Console.WriteLine("Cat meows.");
}
}
// 调用示例
Animal myAnimal = new Dog();
myAnimal.Speak(); // 输出 "Dog barks."
myAnimal = new Cat();
myAnimal.Speak(); // 输出 "Cat meows."
public class Calculator
{
public int Add(int a, int b)
{
return a + b;
}
public double Add(double a, double b)
{
return a + b;
}
}
// 调用示例
Calculator calc = new Calculator();
Console.WriteLine(calc.Add(1, 2)); // 输出 3
Console.WriteLine(calc.Add(1.5, 2.5)); // 输出 4.0
SOLID设计原则 是面向对象编程中常用的五个原则,它们旨在使软件系统更加易维护、易扩展、灵活和可靠。SOLID 是五个原则的首字母缩写:
下面是对每个原则的详细介绍和示例:
定义:
一个类应该只有一个引起变化的原因。也就是说,一个类只负责完成一个职责。
优点:
提高类的内聚性,降低耦合性,使系统更易于维护和理解。
示例:
public class Report
{
public string GetReportData()
{
return "Report Data";
}
}
public class ReportPrinter
{
public void PrintReport(string reportData)
{
Console.WriteLine(reportData);
}
}
// 使用示例
Report report = new Report();
string data = report.GetReportData();
ReportPrinter printer = new ReportPrinter();
printer.PrintReport(data);
在这里,Report
类负责生成数据,ReportPrinter
类负责打印数据,各司其职。
定义:
软件实体(类、模块、函数等)应该对扩展开放,对修改封闭。也就是说,可以通过扩展功能而不是修改现有代码来满足需求。
优点:
减少修改已有代码的风险,遵循“对修改封闭”的原则。
示例:
public abstract class Shape
{
public abstract void Draw();
}
public class Circle : Shape
{
public override void Draw()
{
Console.WriteLine("Drawing Circle");
}
}
public class Rectangle : Shape
{
public override void Draw()
{
Console.WriteLine("Drawing Rectangle");
}
}
// 使用示例
List<Shape> shapes = new List<Shape> { new Circle(), new Rectangle() };
foreach (var shape in shapes)
{
shape.Draw(); // 输出: Drawing Circle, Drawing Rectangle
}
在这个例子中,通过扩展新的 Shape
子类(如 Triangle
),无需修改现有的代码逻辑即可增加新功能。
定义:
子类必须能够替换掉它们的基类而不会导致程序的错误。
优点:
确保系统的扩展是安全的,避免意外行为。
示例:
public class Bird
{
public virtual void Fly()
{
Console.WriteLine("Bird is flying");
}
}
public class Sparrow : Bird
{
public override void Fly()
{
Console.WriteLine("Sparrow is flying");
}
}
public class Penguin : Bird
{
public override void Fly()
{
throw new InvalidOperationException("Penguins can't fly");
}
}
// 改进方式
public abstract class Bird
{
}
public class FlyingBird : Bird
{
public virtual void Fly()
{
Console.WriteLine("Bird is flying");
}
}
public class NonFlyingBird : Bird
{
public virtual void Walk()
{
Console.WriteLine("Bird is walking");
}
}
通过区分 FlyingBird
和 NonFlyingBird
,避免了企鹅继承飞行行为的错误。
定义:
一个类不应该被强迫实现它不需要的接口;接口应该尽可能小而具体。
优点:
减少类对不必要方法的依赖,提高系统灵活性。
示例:
public interface IPrinter
{
void Print();
void Scan();
void Fax();
}
// 改进方式:分离接口
public interface IPrint
{
void Print();
}
public interface IScan
{
void Scan();
}
public interface IFax
{
void Fax();
}
public class MultiFunctionPrinter : IPrint, IScan, IFax
{
public void Print() { Console.WriteLine("Printing"); }
public void Scan() { Console.WriteLine("Scanning"); }
public void Fax() { Console.WriteLine("Faxing"); }
}
public class SimplePrinter : IPrint
{
public void Print() { Console.WriteLine("Printing"); }
}
在改进后,SimplePrinter
只实现它需要的 IPrint
,避免了不必要的方法依赖。
定义:
高层模块不应该依赖低层模块,二者都应该依赖于抽象;抽象不应该依赖细节,细节应该依赖抽象。
优点:
增强模块之间的解耦性,使系统更易于扩展和维护。
示例:
// 违反DIP
public class Light
{
public void TurnOn()
{
Console.WriteLine("Light turned on");
}
}
public class Switch
{
private Light _light = new Light();
public void On()
{
_light.TurnOn();
}
}
// 改进方式
public interface IDevice
{
void TurnOn();
}
public class Light : IDevice
{
public void TurnOn()
{
Console.WriteLine("Light turned on");
}
}
public class Fan : IDevice
{
public void TurnOn()
{
Console.WriteLine("Fan turned on");
}
}
public class Switch
{
private IDevice _device;
public Switch(IDevice device)
{
_device = device;
}
public void On()
{
_device.TurnOn();
}
}
// 使用示例
IDevice light = new Light();
Switch lightSwitch = new Switch(light);
lightSwitch.On(); // 输出 "Light turned on"
通过依赖于 IDevice
接口,Switch
不再依赖具体的 Light
类,从而提高了灵活性。
原则 | 核心思想 |
---|---|
单一职责原则 (SRP) | 每个类只负责一个功能。 |
开放封闭原则 (OCP) | 对扩展开放,对修改封闭。 |
里氏替换原则 (LSP) | 子类可以安全地替换基类而不影响系统功能。 |
接口隔离原则 (ISP) | 使用多个专门的接口,而不是一个大而全的接口。 |
依赖倒置原则 (DIP) | 高层模块和低层模块都依赖于抽象,细节依赖于抽象而非具体实现。 |
遵循 SOLID原则,可以帮助开发者设计出高质量、可维护、易扩展的代码。
CLR(Common Language Runtime)和 BCL(Base Class Library)是 .NET 框架中的两个重要概念,但它们的角色和功能不同:
核心作用:负责执行和管理 .NET 应用程序。
CLR 是 .NET 平台的运行时环境,提供应用程序执行所需的基础支持。
职责
:
理解:CLR 是 .NET 应用程序的运行时引擎,提供底层支持。
核心作用:提供构建 .NET 应用程序的核心功能库。
BCL 是一组预定义的类和接口,开发者可以直接使用这些类来实现应用程序的功能。
主要内容
:
System.Int32
、System.String
等常用数据类型。System.Collections
和 System.Collections.Generic
提供的数组、列表、字典等。System.IO
提供的文件读写功能。System.Net
提供的网络协议和通信支持。System.Threading
提供的线程支持。System.Math
和 System.DateTime
。理解:BCL 是开发 .NET 应用程序的工具包,帮助开发者快速构建功能。
特性 | CLR | BCL |
---|---|---|
定位 | 执行环境 | 应用程序开发的类库 |
功能 | 负责代码执行和资源管理 | 提供各种常用的类和接口 |
作用 | 提供运行时支持 | 提供开发时的工具和功能 |
示例 | 垃圾回收、类型安全、异常处理 | System.String 、System.Collections.List |
关系:
CLR 提供了运行时的支持,而 BCL 为开发者提供了功能丰富的工具,二者共同构成了 .NET 平台运行和开发的基础。
.NET 应用程序是使用 .NET 平台开发的应用程序。它可以是桌面应用、Web 应用、移动应用、云服务等类型。
桌面应用
:
Web 应用
:
移动应用
:
云服务
:
控制台应用:简单的命令行工具或服务。
游戏开发:通过 Unity 引擎使用 .NET/C# 编写游戏逻辑。
.NET 平台是一个支持开发和运行应用程序的统一开发平台,提供了工具、框架和运行时环境。
CLR(Common Language Runtime)
:
BCL(Base Class Library)
:
语言支持
:
开发工具
:
跨平台支持
:
统一的生态系统
:
.NET Framework
:
.NET Core
:
.NET 5+
:
属性 | .NET 平台 | .NET 应用程序 |
---|---|---|
定义 | 支持开发和运行应用的框架和运行时环境 | 使用 .NET 平台开发并运行的具体应用程序 |
包含内容 | 包括 CLR、BCL、语言支持、工具等 | 包括桌面、Web、移动应用等,依赖于 .NET 平台运行 |
目标用户 | 开发者和平台维护者 | 最终用户使用的成品(如桌面工具、Web 应用等) |
示例 | .NET Framework、.NET Core、.NET 5+ | 记事本工具、动态网站、移动购物应用等 |
总结:
在讨论托管代码和非托管代码时,关键是看代码的运行环境和内存管理方式。以下是对 C#、C、C++、Java 和 JavaScript 是否属于托管代码的分析:
定义:
托管代码是由一个运行时环境(如 .NET 的 CLR 或 Java 的 JVM)管理的代码。运行时负责:
定义:
非托管代码直接运行在操作系统上,不依赖运行时环境。开发者需要手动管理内存、处理指针等。
malloc
和 free
)。语言 | 默认运行环境 | 是否托管代码 |
---|---|---|
C# | CLR (.NET 平台) | 是托管代码 |
Java | JVM (Java 平台) | 是托管代码 |
JavaScript | 浏览器或 Node.js 引擎 | 是托管代码 |
C | 操作系统直接运行 | 是非托管代码 |
C++ | 操作系统直接运行 | 是非托管代码(默认) |
DLL
)。在 C# 中,枚举器(Enumerator)和迭代器(Iterator)是处理集合元素时的两个重要概念。它们分别与集合的遍历和自定义集合的遍历行为有关。
枚举器是实现了 System.Collections.IEnumerator
或泛型版本 System.Collections.Generic.IEnumerator
接口的对象,用于遍历集合中的元素。
枚举器提供了一种可以逐一访问集合元素的方法,而无需暴露集合的底层实现。常用的方法包括:
MoveNext()
:移动到集合的下一个元素。如果已经到达集合的结尾,返回 false
。Current
:获取当前元素。Reset()
:将枚举器重置到集合的起始位置(通常不常用,且部分实现可能会抛出异常)。List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
IEnumerator<int> enumerator = numbers.GetEnumerator();
while (enumerator.MoveNext())
{
Console.WriteLine(enumerator.Current);
}
enumerator.Dispose(); // 释放资源
迭代器是一个方法、属性或代码块,使用 yield return
和 yield break
关键字定义,用于控制集合的遍历逻辑。
迭代器的定义隐藏了枚举器的复杂性,提供了一种更简单、声明式的方式来实现 IEnumerable
或 IEnumerable
接口。
以下示例展示了一个简单的迭代器,用于返回偶数序列:
public IEnumerable<int> GetEvenNumbers(int max)
{
for (int i = 0; i <= max; i++)
{
if (i % 2 == 0)
{
yield return i; // 返回当前偶数
}
}
}
使用迭代器:
var evenNumbers = GetEvenNumbers(10);
foreach (var num in evenNumbers)
{
Console.WriteLine(num);
}
yield return
: 暂时返回一个值,同时记住当前的执行位置,以便在下一次调用时继续。yield break
: 提前终止迭代器。IEnumerable
接口的核心机制,提供了集合元素的遍历能力。特性 | 枚举器 | 迭代器 |
---|---|---|
实现复杂度 | 需要手动实现 IEnumerator 接口。 |
使用 yield return ,实现简单。 |
可读性 | 通常较低,逻辑分散且繁琐。 | 简洁明了,可读性高。 |
性能 | 性能稍高,但代码较复杂。 | 性能稍低,使用更灵活。 |
使用场景 | 当需要精确控制遍历逻辑时使用。 | 当需要快速定义集合遍历逻辑时使用。 |
通过理解枚举器和迭代器,开发者可以根据实际需求选择适合的集合遍历方式,写出高效、可维护的代码。
这段代码是 WPF(Windows Presentation Foundation) XAML 代码,它用于在 UI 界面中绑定数据到 TextBlock
控件。
<TextBlock
Text="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:WorkStationPanel}, Path=Title}"
Foreground="White"
HorizontalAlignment="Left"
VerticalAlignment="Top"/>
作用:
这个 TextBlock
会在 UI 中显示 WorkStationPanel
组件的 Title
属性值。
Text="{Binding ...}"
这是 数据绑定(Data Binding) 语法,表示 TextBlock.Text
会绑定到某个数据源的值。
RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:WorkStationPanel}
RelativeSource
:用于绑定到相对位置的元素,而不是数据上下文 (DataContext
)。Mode=FindAncestor
:告诉 TextBlock
向上查找 UI 层级结构中的某个父级元素。AncestorType=local:WorkStationPanel
:
local:WorkStationPanel
是查找的父级 UI 元素的类型(自定义控件)。TextBlock
会沿着 UI 层级向上查找最近的 WorkStationPanel
控件,并获取它的数据。Path=Title
Path=Title
表示从找到的 WorkStationPanel
控件中提取 Title
属性的值,并显示在 TextBlock
中。Foreground="White"
TextBlock
的文本颜色为 白色。HorizontalAlignment="Left"
VerticalAlignment="Top"
假设 WorkStationPanel
是一个自定义控件,定义如下:
public class WorkStationPanel : UserControl
{
public static readonly DependencyProperty TitleProperty = DependencyProperty.Register(
"Title", typeof(string), typeof(WorkStationPanel), new PropertyMetadata("默认标题"));
public string Title
{
get { return (string)GetValue(TitleProperty); }
set { SetValue(TitleProperty, value); }
}
}
在 XAML
界面中:
<local:WorkStationPanel Title="工作站 1">
<TextBlock
Text="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:WorkStationPanel}, Path=Title}"
Foreground="White"
HorizontalAlignment="Left"
VerticalAlignment="Top"/>
local:WorkStationPanel>
TextBlock
的 Text
绑定到了 WorkStationPanel.Title
,如果 Title="工作站 1"
,那么 TextBlock
就会显示 “工作站 1”。✅ FindAncestor 让 TextBlock
可以向上查找父级元素,而不是依赖 DataContext
。
✅ Text="{Binding Path=Title}"
使 TextBlock
显示 WorkStationPanel
的 Title
。
✅ 这个方式适用于自定义控件或复杂 UI 结构,确保数据能正确传递。
你是要在项目里用这个绑定吗?如果有具体需求,我可以帮你优化
在 C# 中,BeginInvoke
和 Invoke
都用于在 UI 线程或其他线程上执行委托,但它们的执行方式不同。
Invoke
(同步调用)Invoke
同步执行,当前线程会等待方法执行完成后再继续运行。Invoke
切换到 UI 线程。private void UpdateUI()
{
if (this.InvokeRequired) // 检查是否在 UI 线程外调用
{
this.Invoke(new Action(UpdateUI)); // 切换到 UI 线程
return;
}
label1.Text = "更新 UI"; // UI 只能在主线程修改
}
✅ 调用 Invoke
时,当前线程会停下来,等 UpdateUI
执行完毕后才继续。
BeginInvoke
(异步调用)BeginInvoke
异步执行,调用后立即返回,不会阻塞当前线程。private void UpdateUI()
{
if (this.InvokeRequired)
{
this.BeginInvoke(new Action(UpdateUI)); // 异步执行,不会阻塞
return;
}
label1.Text = "更新 UI";
}
✅ 调用 BeginInvoke
后,主线程不会等待 UpdateUI
运行完毕,而是继续执行后续代码。
特性 | Invoke (同步) |
BeginInvoke (异步) |
---|---|---|
执行方式 | 同步(阻塞当前线程) | 异步(立即返回,不阻塞) |
是否等待结果 | 是,直到方法执行完毕 | 否,直接返回 |
适用场景 | 需要立即获取执行结果 | 不关心执行结果,提高并发性 |
是否会卡 UI | 可能会卡顿(等待执行完成) | 不会卡 UI,适合耗时操作 |
Invoke
BeginInvoke
不支持)。Invoke
确保 UI 线程同步更新)。BeginInvoke
BeginInvoke
如何获取返回值?BeginInvoke
自己不会等待方法执行完,因此如果你需要获取结果,可以使用 回调函数 或 EndInvoke
。
EndInvoke
获取返回值private void StartAsyncTask()
{
Func<int, int, int> addFunc = (a, b) => a + b;
// 异步调用 addFunc(10, 20)
IAsyncResult asyncResult = addFunc.BeginInvoke(10, 20, null, null);
// 其他代码可以继续运行,而不会等待
// 在需要获取结果的地方调用 EndInvoke(会阻塞等待结果)
int result = addFunc.EndInvoke(asyncResult);
Console.WriteLine("计算结果:" + result);
}
✅ EndInvoke
会阻塞当前线程,直到异步方法执行完毕。
Task
和 BeginInvoke
哪个更好?在 .NET 4.0 之后,Task
和 async/await
逐渐取代 BeginInvoke
,因为:
Task
更加现代化,支持更强的并发。async/await
代码更清晰,更容易理解和维护。Task.Run
替代 BeginInvoke
Task.Run(() =>
{
Console.WriteLine("异步任务执行中...");
});
使用场景 | Invoke |
BeginInvoke |
---|---|---|
UI 线程调用 | ✅ 确保同步执行 | ✅ 不卡 UI,异步执行 |
需要返回值 | ✅ 可以直接返回 | ❌ 需要 EndInvoke 获取 |
可能阻塞线程 | ❌ 可能导致 UI 卡顿 | ✅ 不会阻塞 |
推荐替代方案 | Task.Run() (更现代) |
async/await |
如果你的代码是新项目,建议用 Task.Run()
,而不是 BeginInvoke
async/await
的使用async/await
是 C# 异步编程的核心,可以让代码执行非阻塞操作,提高性能和 UI 响应速度。
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
Console.WriteLine("任务开始...");
await DoSomethingAsync(); // 等待异步方法执行完成
Console.WriteLine("任务完成!");
}
static async Task DoSomethingAsync()
{
Console.WriteLine("正在执行任务...");
await Task.Delay(2000); // 异步等待 2 秒
Console.WriteLine("任务执行完毕!");
}
}
任务开始...
正在执行任务...
(等待 2 秒)
任务执行完毕!
任务完成!
✅ await
等待异步任务完成,但不会阻塞主线程。
async
和 await
规则 async
关键字:用于标记方法是异步方法,返回类型必须是:
Task
(无返回值)Task
(有返回值)void
(不推荐) await
关键字:用于异步等待一个 Task
,不会阻塞线程。
async/await
处理返回值如果 async
方法需要返回值,应该使用 Task
。
static async Task<int> GetDataAsync()
{
Console.WriteLine("获取数据...");
await Task.Delay(2000); // 模拟耗时任务
return 100; // 返回数据
}
static async Task Main()
{
int result = await GetDataAsync();
Console.WriteLine($"获取的数据是:{result}");
}
✅ await GetDataAsync()
自动获取返回值,不像 Task.Run().Result
那样会阻塞线程。
async/await
不会阻塞 UI如果你在 Windows 窗体(WinForms)或 WPF 中更新 UI,需要使用 await
,否则 UI 会卡住。
Task.Result
,会阻塞 UIprivate void Button_Click(object sender, EventArgs e)
{
label1.Text = "获取中...";
int result = GetDataAsync().Result; // UI 线程卡死
label1.Text = $"结果:{result}";
}
❗ 这样写会让 UI 界面卡死 2 秒!
await
异步执行private async void Button_Click(object sender, EventArgs e)
{
label1.Text = "获取中...";
int result = await GetDataAsync(); // UI 线程不会卡住
label1.Text = $"结果:{result}";
}
✨ await
让 UI 线程不被阻塞,避免卡顿!
Task.Run()
结合 async/await
如果是计算密集型任务(如大数据处理),建议使用 Task.Run()
在后台线程执行,避免主线程卡顿。
private async void Button_Click(object sender, EventArgs e)
{
label1.Text = "计算中...";
int result = await Task.Run(() => HeavyComputation()); // 在后台线程运行
label1.Text = $"计算结果:{result}";
}
private int HeavyComputation()
{
// 模拟耗时计算
System.Threading.Thread.Sleep(3000);
return 42;
}
✅ Task.Run()
在后台线程执行,UI 线程不卡顿。
async/await
处理异常 async
方法可以使用 try-catch
捕获异常,不会导致程序崩溃。
static async Task<int> GetDataWithErrorAsync()
{
await Task.Delay(1000);
throw new Exception("数据获取失败!");
}
static async Task Main()
{
try
{
int result = await GetDataWithErrorAsync();
Console.WriteLine($"结果:{result}");
}
catch (Exception ex)
{
Console.WriteLine($"发生异常:{ex.Message}");
}
}
✅ await
会传播异常,try-catch
可捕获并处理。
async void
的问题async void
不推荐!private async void DoSomething()
{
await Task.Delay(1000);
throw new Exception("错误!");
}
async void
不能被 try-catch
捕获,错误会直接崩溃程序!
✅ 用 async Task
代替
private async Task DoSomething()
{
await Task.Delay(1000);
throw new Exception("错误!");
}
如果 async
方法需要被 await
,必须返回 Task
,不能用 void
。
await Task.WhenAll()
和 await Task.WhenAny()
Task.WhenAll()
(等待所有任务完成)var task1 = GetDataAsync();
var task2 = GetDataAsync();
await Task.WhenAll(task1, task2);
Console.WriteLine("所有任务完成!");
多个异步任务并发执行,只有全部完成后才继续执行。
Task.WhenAny()
(等待其中一个完成)var task1 = GetDataAsync();
var task2 = GetDataAsync();
await Task.WhenAny(task1, task2);
Console.WriteLine("有一个任务完成了!");
适用于等待最快完成的任务,不阻塞其他任务。
关键点 | 说明 |
---|---|
async |
用于标记方法为 异步 |
await |
等待 异步方法完成,不阻塞主线程 |
Task |
async 方法返回值的标准写法 |
async void |
不推荐! 会导致异常无法捕获 |
Task.Run() |
适用于计算密集型任务 |
Task.WhenAll() |
等待多个任务完成 |
Task.WhenAny() |
等待其中一个任务完成 |
try-catch |
处理 async 方法异常 |
await
,避免 Task.Result
造成界面卡死。Task
代替 async void
,防止异常无法捕获。Task.Run()
避免主线程卡死。Task.WhenAll()
提高性能。Dispatcher.Invoke
的作用在 WPF(Windows Presentation Foundation) 中,UI 线程(主线程) 负责界面更新,但如果你在 后台线程(例如 Task.Run()
)中尝试更新 UI,就会抛出异常:
❌ 错误示例(在后台线程直接修改 UI)
private async void Button_Click(object sender, RoutedEventArgs e)
{
await Task.Run(() =>
{
// ❌ 直接修改 UI,会报错
label1.Content = "更新UI";
});
}
原因:WPF 规定 UI 只能在主线程(Dispatcher 线程)修改!
Dispatcher.Invoke
解决 UI 线程问题Dispatcher.Invoke
可以让后台线程安全地调用 UI 线程!
private async void Button_Click(object sender, RoutedEventArgs e)
{
await Task.Run(() =>
{
Dispatcher.Invoke(() =>
{
label1.Content = "更新UI"; // ✅ 让 UI 在线程安全的情况下更新
});
});
}
Dispatcher.Invoke()
会把代码切换回 UI 线程,然后执行。
Dispatcher.Invoke()
VS Dispatcher.BeginInvoke()
方法 | 作用 | 是否阻塞 |
---|---|---|
Dispatcher.Invoke(Action) |
同步调用 UI 线程,代码执行完才能返回 | ✅ 阻塞 |
Dispatcher.BeginInvoke(Action) |
异步调用 UI 线程,不等待 UI 更新完就继续执行 | 不阻塞 |
Dispatcher.Invoke()
(同步执行,阻塞)会阻塞当前线程,直到 UI 线程执行完!
Dispatcher.Invoke(() =>
{
label1.Content = "更新UI";
});
Console.WriteLine("UI 已更新!");
✅ UI 确保更新后,代码才继续执行。
Dispatcher.BeginInvoke()
(异步执行,不阻塞)不会等待 UI 更新完,代码继续运行
Dispatcher.BeginInvoke(() =>
{
label1.Content = "更新UI";
});
Console.WriteLine("UI 可能还没更新,但代码继续运行!");
✅ 不会卡住当前线程,提高性能。
Dispatcher.Invoke()
使用示例private async void Button_Click(object sender, RoutedEventArgs e)
{
await Task.Run(() =>
{
// 耗时操作(模拟计算)
Thread.Sleep(2000);
// 回到 UI 线程更新 UI
Dispatcher.Invoke(() =>
{
label1.Content = "计算完成!";
});
});
}
✅ 避免 UI 线程卡死,同时保证 UI 更新安全!
Application.Current.Dispatcher
在 WPF 的任何地方都可以使用:
Application.Current.Dispatcher.Invoke(() =>
{
label1.Content = "更新UI";
});
✅ 适用于非 UI 线程的代码,如 Task.Run()
或 BackgroundWorker
。
方法 | 作用 | 是否阻塞 |
---|---|---|
Dispatcher.Invoke() |
同步更新 UI,代码等 UI 更新完再继续 | ✅ 阻塞 |
Dispatcher.BeginInvoke() |
异步更新 UI,不等待 UI 更新完就继续执行 | 不阻塞 |
如果要等 UI 更新后再继续执行代码 用 Dispatcher.Invoke()
如果不关心 UI 是否更新完毕,提高性能 用 Dispatcher.BeginInvoke()
用 Dispatcher.Invoke()
可以安全地跨线程更新 UI!
概念:
我们可以等待一个异步任务完成,但不会阻塞主线程,即 主线程仍然可以执行其他任务,而不会卡住界面或程序。
await
,不阻塞主线程private async void Button_Click(object sender, RoutedEventArgs e)
{
label1.Content = "任务进行中...";
// ❗等待异步任务完成,但不会阻塞 UI 线程
await Task.Delay(5000);
label1.Content = "任务完成!";
}
await Task.Delay(5000);
Task.Wait()
,导致 UI 卡死private void Button_Click(object sender, RoutedEventArgs e)
{
label1.Content = "任务进行中...";
// ❗错误:这会阻塞主线程,导致 UI 卡死!
Task.Delay(5000).Wait();
label1.Content = "任务完成!";
}
Task.Delay(5000).Wait();
结论:在 UI 线程中,应该使用 await
,不要用 .Wait()
!
Task.Run
+ await
,不阻塞 UI如果是 CPU 密集型任务(如计算、数据库查询),可以用 Task.Run()
在后台线程执行:
private async void Button_Click(object sender, RoutedEventArgs e)
{
label1.Content = "计算中...";
// ❗在后台线程运行耗时计算,但不会阻塞 UI
int result = await Task.Run(() =>
{
Thread.Sleep(5000); // 模拟计算
return 42;
});
label1.Content = $"计算完成:{result}";
}
Task.Run()
在后台线程运行计算,不影响 UI。await
等待结果 (但不阻塞 UI 线程)。方法 | 是否阻塞 UI | UI 是否流畅 |
---|---|---|
await Task.Delay(5000); |
❌ 不阻塞 | ✅ 流畅 |
Task.Delay(5000).Wait(); |
✅ 阻塞 | ❌ 卡死 |
await Task.Run(() => { 耗时任务 }); |
❌ 不阻塞 | ✅ 流畅 |
✅ 推荐做法:用 await
等待异步任务,不会阻塞主线程,UI 仍然流畅!
async
和 await
的作用async
和 await
用于异步编程,它们可以让代码在等待耗时操作时不阻塞主线程,从而提高程序的响应性和性能。
当代码中有以下需求时,应该使用 async/await
:
✅ 避免 UI 卡死:在 WPF/WinForms 等 UI 线程中,避免界面无响应
✅ 避免阻塞主线程:防止等待网络请求、文件读写、数据库操作时程序挂起
✅ 执行 I/O 操作:例如网络请求、数据库查询、文件读写等
✅ 在后台运行计算:避免 CPU 密集型任务影响 UI 响应
async/await
能解决什么问题?如果执行一个 耗时操作(如 5 秒计算),但 UI 不能卡死,该怎么办?
private void Button_Click(object sender, RoutedEventArgs e)
{
label1.Content = "计算中...";
// ❗错误:在主线程执行,会导致 UI 无响应
Thread.Sleep(5000);
label1.Content = "计算完成!";
}
问题:
Thread.Sleep(5000);
阻塞 UI 线程,用户无法点击按钮、拖动窗口,程序像“卡住”了一样。async/await
,不阻塞 UI)private async void Button_Click(object sender, RoutedEventArgs e)
{
label1.Content = "计算中...";
// ✅ 正确:异步等待 5 秒,不阻塞 UI
await Task.Delay(5000);
label1.Content = "计算完成!";
}
✅ UI 仍然流畅,用户可以继续操作!
如果计算任务 很耗时,怎么避免影响 UI?
private void Button_Click(object sender, RoutedEventArgs e)
{
label1.Content = "计算中...";
// ❗错误:在 UI 线程执行计算,导致界面卡死
int result = HeavyCalculation();
label1.Content = $"计算完成:{result}";
}
private int HeavyCalculation()
{
Thread.Sleep(5000); // 模拟计算
return 42;
}
问题:HeavyCalculation()
运行在 UI 线程,导致界面卡死!
Task.Run()
在后台运行计算)private async void Button_Click(object sender, RoutedEventArgs e)
{
label1.Content = "计算中...";
// ✅ 正确:在后台线程运行计算,不影响 UI
int result = await Task.Run(() => HeavyCalculation());
label1.Content = $"计算完成:{result}";
}
private int HeavyCalculation()
{
Thread.Sleep(5000); // 模拟计算
return 42;
}
✅ 计算在后台线程运行,UI 仍然流畅!
如果程序需要请求网络数据(如 HTTP API),但 UI 不能卡死?
private void Button_Click(object sender, RoutedEventArgs e)
{
label1.Content = "请求数据中...";
// ❗错误:同步请求,UI 卡死
string data = GetData();
label1.Content = $"数据:{data}";
}
private string GetData()
{
using (var client = new HttpClient())
{
return client.GetStringAsync("https://api.example.com/data").Result; // ❌ 阻塞
}
}
问题:.Result
会阻塞 UI 线程,导致界面卡住!
async/await
让 UI 不卡死)private async void Button_Click(object sender, RoutedEventArgs e)
{
label1.Content = "请求数据中...";
// ✅ 正确:异步请求数据,不阻塞 UI
string data = await GetDataAsync();
label1.Content = $"数据:{data}";
}
private async Task<string> GetDataAsync()
{
using (var client = new HttpClient())
{
return await client.GetStringAsync("https://api.example.com/data");
}
}
✅ 异步执行网络请求,UI 不会卡死!
文件 I/O 操作通常较慢,建议使用 async/await
。
private async void Button_Click(object sender, RoutedEventArgs e)
{
label1.Content = "读取文件中...";
// ✅ 正确:异步读取文件
string content = await File.ReadAllTextAsync("data.txt");
label1.Content = $"文件内容:{content}";
}
✅ 不会阻塞 UI,文件读取完成后再更新界面!
async/await
?场景 | 解决方案 |
---|---|
UI 操作不能卡死 | await Task.Delay() 避免 UI 卡死 |
耗时计算不能影响 UI | await Task.Run() 在后台执行 |
发送 HTTP 请求 | await HttpClient.GetStringAsync() |
读取/写入文件 | await File.ReadAllTextAsync() |
数据库查询 | await dbContext.Users.ToListAsync() |
✅ async/await
主要用来 处理耗时任务,但不阻塞 UI 或主线程。
✅ 适用于网络请求、文件读写、数据库查询、后台计算等场景。
✅ 避免 UI 卡死,让程序更流畅!
在主线程中不用 await
,而是用 new Task(() => {}).Start();
来执行异步任务,确实不会阻塞主线程,但这样做有一些潜在问题。
new Task().Start()
vs. await
我们分别用 new Task().Start()
和 await
来执行任务,看看它们的区别。
new Task().Start()
private void Button_Click(object sender, RoutedEventArgs e)
{
label1.Content = "任务进行中...";
// ❗在新线程中运行任务
new Task(() =>
{
Thread.Sleep(5000); // 模拟耗时任务
Dispatcher.Invoke(() => label1.Content = "任务完成!"); // ❗需要手动更新 UI
}).Start();
}
✅ 不会阻塞 UI,但需要手动用 Dispatcher.Invoke()
更新 UI,否则会报错(因为 UI 只能在主线程更新)。
⚠ 不支持异常捕获,如果 Task
内部发生异常,不会自动抛出到主线程,会导致程序崩溃。
await Task.Run()
private async void Button_Click(object sender, RoutedEventArgs e)
{
label1.Content = "任务进行中...";
// ✅ 用 `await Task.Run()` 运行任务,自动回到 UI 线程
await Task.Run(() => Thread.Sleep(5000));
label1.Content = "任务完成!";
}
✅ 不会阻塞 UI,任务完成后自动回到 UI 线程,无需手动 Dispatcher.Invoke()
。
✅ 支持异常传播,如果任务内发生异常,会正确抛出到 try-catch
处理。
new Task().Start()
vs. await Task.Run()
区别方式 | 是否阻塞 UI | 线程调度 | UI 更新 | 异常捕获 |
---|---|---|---|---|
new Task().Start() |
❌ 不会 | 任务运行在新线程 | ❗需要 Dispatcher.Invoke() |
❌ 不会传播异常 |
await Task.Run() |
❌ 不会 | 任务运行在线程池线程 | ✅ 自动回到 UI 线程 | ✅ 可以 try-catch 处理异常 |
new Task().Start()
✅ new Task().Start()
适合:
不适合的情况:
Task.Run()
更安全)Task.Run()
能自动传播异常)new Task().Start()
可能会创建过多线程,影响性能)1️⃣ **如果只是让任务异步运行,不阻塞 UI,**用 await Task.Run()
更合适。
2️⃣ **如果任务不涉及 UI,且不需要 await
,**可以用 Task.Run(() => {...})
。
3️⃣ **如果需要手动管理线程(不推荐),**可以用 new Task().Start()
,但要注意 UI 更新和异常处理。
private async void Button_Click(object sender, RoutedEventArgs e)
{
label1.Content = "任务进行中...";
try
{
// ✅ `await Task.Run()` 运行任务,不阻塞 UI,支持异常捕获
await Task.Run(() => Thread.Sleep(5000));
label1.Content = "任务完成!";
}
catch (Exception ex)
{
label1.Content = "任务失败:" + ex.Message;
}
}
✅ 不会阻塞 UI
✅ 自动回到 UI 线程
✅ 可以 try-catch
处理异常
最佳实践!