目录
示例 2-.NET 6 API -servicecollections (DI)
11. 使用LINQ查询并返回前100条记录 (EF Core)
可空引用类型(Nullable Reference Types, C# 8.0及更高版本)
Generic(泛型)
C# 中的泛型(Generics)是 .NET Framework 2.0 引入的一个非常强大的特性,它允许你定义类、接口、方法、委托等时,指定一个或多个类型参数(Type Parameters)。这些类型参数在类、接口、方法等的声明中用作占位符,在实例化泛型类型或调用泛型方法时,必须用具体的类型替换这些占位符。这样做的好处包括代码复用、类型安全以及更好的性能。
泛型类的基本用法
假设我们有一个简单的类,用于存储两个相等的对象,我们可以使用泛型来创建这个类,使得它可以存储任何类型的两个对象。
public class Pair<T>
{
public T First { get; set; }
public T Second { get; set; }
public Pair(T first, T second)
{
First = first;
Second = second;
}
}
// 使用泛型类
Pair<int> intPair = new Pair<int>(1, 2);
Pair<string> stringPair = new Pair<string>("Hello", "World");
在这个例子中,
Pair<T>
是一个泛型类,其中T
是一个类型参数。在创建Pair
类的实例时,我们可以指定T
的具体类型,比如int
或string
。
泛型方法
除了泛型类,C# 还支持泛型方法。泛型方法允许你在方法签名中指定一个或多个类型参数。
public class GenericMethods
{
// 泛型方法
public static void Foo<T>(ref T lhs, ref T rhs)
{
T temp = lhs;
lhs = rhs;
rhs = temp;
}
// 使用泛型方法
static void Main()
{
int a = 1, b = 2;
Foo(ref a, ref b);
Console.WriteLine($"a = {a}, b = {b}");
string str1 = "Hello", str2 = "World";
Foo(ref str1, ref str2);
Console.WriteLine($"str1 = {str1}, str2 = {str2}");
}
}
在这个例子中,
Foo<T>
是一个泛型方法,它接受两个类型为T
的参数(通过ref
关键字传递),并在方法内部交换它们的值。由于T
是一个类型参数,Swap
方法可以用于任何类型的变量。
public class Utilities<T> where T : IComparable, new()
{
public int Max(int a, int b)
{
return a > b ? a : b;
}
public void DoSomething(T value)
{
var obj = new T();
}
public T Max(T a, T b)
{
return a.CompareTo(b) > 0 ? a : b;
}
}
public class Product
{
public string Title { get; set; }
public float Price { get; set; }
}
public class DiscountCalculator<TProduct> where TProduct : Product
{
public float CalculateDiscount(TProduct product)
{
return product.Price;
}
}
Delegate
C# 中的委托(Delegate)是一种类型,它定义了方法的类型,使得可以将方法当作另一个方法的参数、返回类型或变量类型。委托是 C# 中实现事件和回调的基础。它们提供了一种类型安全的方式来封装方法引用,使得可以在运行时动态地调用这些方法。
委托的声明
委托的声明类似于方法的声明,但它前面有一个 delegate
关键字。委托可以指向具有相同签名(即相同返回类型和参数列表)的任何方法。
public delegate void MyDelegate(string message);
这个声明定义了一个名为
MyDelegate
的委托类型,它可以指向任何接受一个string
参数并返回void
的方法
委托的实例化
委托对象可以使用 new
关键字后跟委托类型和要引用的方法名来实例化。
MyDelegate del = new MyDelegate(MyMethod);
//或者,从 C# 2.0 开始,可以使用更简洁的语法(称为匿名方法或方法组转换):
MyDelegate del = MyMethod; // 方法组转换
//或者,使用 Lambda 表达式(从 C# 3.0 开始):
MyDelegate del = message => Console.WriteLine(message);
委托的调用
委托对象可以像方法一样被调用,并且会执行它所引用的方法。
del("Hello, Delegate!");
委托的链式调用
委托还支持链式调用,即一个委托可以引用多个方法。当调用这样的委托时,它会按顺序调用所有引用的方法。
MyDelegate del1 = MyMethod1;
MyDelegate del2 = MyMethod2;
// 将两个委托组合成一个委托
MyDelegate delChain = del1 + del2;
// 调用时,会先调用 MyMethod1,然后调用 MyMethod2
delChain("Hello, Chain!");
委托与事件
在 C# 中,事件通常是通过委托来实现的。事件是一种使类或对象能够提供通知的机制。当类中的某些事情发生时(例如,按钮被点击),它可以触发一个事件,并且任何对该事件感兴趣的对象都可以订阅该事件并在事件发生时收到通知。
示例
下面是一个简单的委托示例,展示了如何声明委托、实例化委托以及调用委托引用的方法。
using System;
delegate void MyDelegate(string message);
class Program
{
static void Main(string[] args)
{
MyDelegate del = DisplayMessage;
del("Hello, Delegate!");
// 使用 Lambda 表达式
MyDelegate lambdaDel = message => Console.WriteLine($"Lambda: {message}");
lambdaDel("Hello, Lambda!");
}
static void DisplayMessage(string message)
{
Console.WriteLine($"DisplayMessage: {message}");
}
}
Lambda
在C#中,Lambda表达式是委托的一种非常便捷和强大的表示方式。Lambda表达式通常用于编写内联的匿名方法,它们可以分配给委托类型的变量或者作为方法的参数传递给期望委托作为参数的方法。
Lambda表达式的基本语法
Lambda表达式的基本语法如下:
(参数列表) => 表达式体
或者,如果Lambda表达式包含多条语句,则可以使用大括号来定义语句块,并且需要显式地指定返回类型(尽管在某些情况下,编译器可以推断出返回类型):
(参数列表) => {
// 语句块
return 表达式;
}
Lambda表达式与委托
Lambda表达式经常与委托一起使用,因为它们提供了一种简洁的方式来创建委托实例。由于Lambda表达式本质上是一个匿名方法,因此它们可以直接赋值给任何与之兼容的委托类型的变量。
Lambda表达式在委托链式调用中的应用
在委托的链式调用中,Lambda表达式同样可以发挥重要作用。通过Lambda表达式,我们可以轻松地将多个操作组合成一个委托链,从而在单个委托调用中执行多个操作。
例如,假设我们有一个委托类型MyDelegate
,它定义了一个接受string
参数并返回void
的方法签名。我们可以使用Lambda表达式来创建多个这样的方法,并将它们链接在一起:
delegate void MyDelegate(string message);
// 创建委托实例
MyDelegate del1 = message => Console.WriteLine($"Del1: {message}");
MyDelegate del2 = message => Console.WriteLine($"Del2: {message.ToUpper()}");
// 将两个委托链接在一起
MyDelegate delChain = del1 + del2;
// 调用链式委托
delChain("Hello, Lambda Chain!");
在这个例子中,
delChain
委托在调用时会先执行del1
引用的Lambda表达式(打印原始消息),然后执行del2
引用的Lambda表达式(打印大写消息)。
总结
Lambda表达式是C#中一种非常强大且灵活的特性,它们与委托的结合使用使得我们能够以简洁而富有表达力的方式编写代码。在委托的链式调用中,Lambda表达式提供了一种便捷的方式来定义和操作多个操作序列,从而提高了代码的可读性和可维护性。
Event
在C#中,事件(Event)是一种使类或对象能够提供通知的机制。当类中的某些事情发生时(例如,按钮被点击、数据发生更改等),它可以触发一个事件,并且任何对该事件感兴趣的对象都可以订阅(或监听)该事件,并在事件发生时收到通知。这种机制允许类与其使用者之间实现松耦合,因为类的使用者不需要知道类的内部实现细节,只需订阅它们感兴趣的事件即可。
事件的声明
在C#中,事件通常是通过声明一个事件成员来定义的,该成员使用event
关键字,并且其类型是一个委托。这个委托定义了可以订阅该事件的方法的签名。
public delegate void MyEventHandler(object sender, MyEventArgs e);
public class MyEventArgs : EventArgs
{
// 可以添加一些属性或字段来传递事件相关的数据
public string Message { get; set; }
public MyEventArgs(string message)
{
Message = message;
}
}
public class MyClass
{
// 声明事件成员
public event MyEventHandler MyEvent;
// 触发事件的方法
protected virtual void OnMyEvent(MyEventArgs e)
{
MyEventHandler handler = MyEvent;
if (handler != null)
{
handler(this, e);
}
}
// 当某个条件满足时,触发事件
public void DoSomething()
{
// ... 执行一些操作
OnMyEvent(new MyEventArgs("Hello, Event!"));
}
}
事件的订阅和取消订阅
任何外部类或对象都可以订阅一个类的事件,方法是将一个与事件委托兼容的方法(或Lambda表达式)附加到事件上。同样,也可以随时取消订阅事件。
class Program
{
static void Main(string[] args)
{
MyClass myObject = new MyClass();
// 订阅事件
myObject.MyEvent += MyEventHandlerMethod;
// 或者使用Lambda表达式
myObject.MyEvent += (sender, e) => Console.WriteLine($"Lambda: {e.Message}");
// 触发事件
myObject.DoSomething();
// 取消订阅事件
myObject.MyEvent -= MyEventHandlerMethod;
// 再次触发事件,这次不会调用已取消订阅的方法
myObject.DoSomething();
}
static void MyEventHandlerMethod(object sender, MyEventArgs e)
{
Console.WriteLine($"Method: {e.Message}");
}
}
注意事项
- 在触发事件时,最好先检查事件委托是否为
null
,以避免在没有任何订阅者的情况下抛出NullReferenceException
。 - 委托的
+=
和-=
操作符用于订阅和取消订阅事件。最好在嗲用+=前先调用-=避免重复注册事件。 - 事件是特殊的多播委托,它们只能由声明它们的类在其内部触发。外部代码不能“触发”事件,但可以订阅或取消订阅它们。
- 事件参数类(如上面的
MyEventArgs
)通常继承自System.EventArgs
,并用于向事件处理程序传递额外信息。 - 从C# 6.0开始,可以使用
null
条件运算符来简化事件触发逻辑,例如MyEvent?.Invoke(this, e);
,但这并不是标准的做法,因为Invoke
方法并不是事件触发时的推荐方式(直接调用委托通常更简洁)。然而,在一些特殊情况下,Invoke
方法可能更合适。
Entension methods
C# 中的扩展方法(Extension Methods)是一种特殊的静态方法,它们允许你为现有的类型添加新的方法,而无需创建新的派生类型、修改原始类型或使用任何特殊的工具(如 ILWeaving)。这极大地提高了代码的复用性和灵活性。
如何定义扩展方法
- 静态类:扩展方法必须定义在静态类中。
- 静态方法:扩展方法是静态的。
- 第一个参数:扩展方法的第一个参数必须包括
this
关键字,以指示该方法是对该类型的扩展。此参数类型是你想要扩展的类型。 - 访问级别:扩展方法可以有与其他静态成员相同的访问级别。
使用场景
扩展方法非常适合于为现有类型添加辅助方法,特别是当你没有权限修改这些类型或者这些类型来自第三方库时。通过为这些类型添加扩展方法,你可以提高代码的可读性和可维护性,同时避免创建大量继承自这些类型的子类。
示例 1-String entension
假设我们想要为 string
类型添加一个 ReverseWords
方法,该方法将字符串中的单词顺序反转,但保持每个单词内的字符顺序不变。
using System;
using System.Collections.Generic;
using System.Linq;
public static class StringExtensions
{
// 扩展string类型
public static string ReverseWords(this string str)
{
// 使用LINQ来反转单词顺序
return string.Join(" ", str.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries)
.Reverse());
}
}
class Program
{
static void Main()
{
string sentence = "Hello World from C#";
string reversedWords = sentence.ReverseWords(); // 使用扩展方法
Console.WriteLine(reversedWords); // 输出: "C# from World Hello"
}
}
示例 2-.NET 6 API -servicecollections (DI)
using Demo.Data.DapperRepository;
using Demo.Domain.Dtos;
using Demo.Infrastructure;
using Demo.Infrastructure.Interfaces;
using Demo.Infrastructure.Tokens;
using Demo.Interfaces;
using Demo.Managers;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using MongoDB.Driver;
namespace Demo
{
public static class UseCustomServiceExtension
{
/// <summary>
/// Register all services/managers
/// </summary>
/// <param name="services"></param>
/// <param name="configuration"></param>
/// <exception cref="ArgumentNullException"></exception>
public static void UseCustomServices(this IServiceCollection services, IConfiguration configuration)
{
if (services == null) throw new ArgumentNullException(nameof(services));
if (configuration == null) throw new ArgumentNullException(nameof(configuration));
//Below services used to get token
services.AddHttpContextAccessor();
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddSingleton<IPrincipalAccessor, PrincipalAccessor>();
services.Configure<MongoDbSettings>(configuration.GetSection("MongoDb"));
// Register MongoDbContext as a singleton
services.AddSingleton<MongoDbContext>();
services.AddScoped<IAppSettingHelper, AppSettingHelper>();
services.AddScoped<IJWTTokenHelper, JWTTokenHelper>();
services.AddMemoryCache();
services.AddTransient<ISqlConnectionResolver, SqlConnectionResolver>();
//Register Dapper Repository
services.AddTransient<IReadWriteDapperRepository, ReadWriteDapperRepository>();
services.AddTransient<IReadOnlyDapperRepository, ReadOnlyDapperRepository>();
services.AddTransient<IAccountManager, AccountManager>();
services.AddTransient<IDealDetailsManager, DealDetailsManager>();
services.AddTransient<IEmailTemplateManager, EmailTemplateManager>();
}
}
}
Call demo in Program.cs in .NET6 API
builder.Services.UseCustomServices(builder.Configuration);
注意事项
- 扩展方法是静态解析的,即它们是在编译时绑定到类型的,而不是在运行时。这意味着你不能通过重写扩展方法来“覆盖”它们,也不能在扩展方法中使用虚调用(
virtual
/override
)。 - 扩展方法不能访问它们扩展的类型的私有成员。
- 如果在类型自身中定义了同名方法,则类型自身的方法会隐藏扩展方法。
- 可以在任何命名空间中使用扩展方法,但为了使扩展方法可见,需要导入定义它们的命名空间。
Linq
C# 中的 LINQ(Language Integrated Query)是微软在.NET Framework 3.5中引入的一项技术,它允许开发者以声明性方式编写查询,这些查询可以针对多种数据源执行,包括但不限于数组、集合、XML 文档、数据库等。LINQ 提供了统一的查询语法,让开发者能够以一种更加自然和直观的方式来操作数据集合。
LINQ 的类型
LINQ 主要分为以下几种类型:
- LINQ to Objects:用于查询和操作内存中的集合,如数组、List<T> 等。
- LINQ to SQL(现已被 Entity Framework 替代):用于将 LINQ 查询直接转换为 SQL 查询,从而实现对数据库的操作。
- LINQ to XML:用于在内存中创建、修改和查询 XML 数据。
- LINQ to DataSet:用于在 DataSet 和 DataTable 上执行查询。
- LINQ to Entities(Entity Framework 的一部分):用于在 Entity Framework 实体上执行查询。
LINQ 查询语法
LINQ 查询使用类似 SQL 的查询语法,但它是以方法调用的形式嵌入到 C# 语言中的。查询操作包括 from
、where
、select
、group by
、order by
等子句,这些子句可以链式调用以构建复杂的查询。
示例:LINQ to Objects
假设我们有一个整数列表,并希望找到其中所有的偶数:
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// 使用 LINQ 查询语法
var evenNumbers = from n in numbers
where n % 2 == 0
select n;
// 或者使用方法语法
var evenNumbersMethodSyntax = numbers.Where(n => n % 2 == 0);
foreach (var num in evenNumbers)
{
Console.WriteLine(num);
}
// 方法语法同样有效
foreach (var num in evenNumbersMethodSyntax)
{
Console.WriteLine(num);
}
}
}
LINQ 特性
- 延迟执行:大多数 LINQ 查询在调用
ToList()
,ToArray()
,First()
,Single()
等方法之前都不会执行。这称为“延迟执行”,它允许你构建复杂的查询,只有在真正需要结果时才执行它们。 - 链式操作:LINQ 查询是可组合的,你可以将多个查询操作链接在一起,以构建复杂的查询逻辑。
- 强类型:LINQ 查询在编译时会进行类型检查,这有助于在编写代码时捕获错误。
- 表达能力:LINQ 提供了丰富的查询操作,几乎可以表达任何类型的数据转换和查询需求。
通过使用 LINQ,C# 开发者能够以更加简洁和表达力强的方式处理数据集合,从而提高开发效率和代码质量。
LINQ 常见用法
1. 筛选(Filtering)
使用 Where
方法根据特定条件筛选集合中的元素。
var filteredList = list.Where(item => item.Condition == true).ToList();
2. 投影(Projection)
使用 Select
方法从集合中选择某些属性或进行转换。
var projection = list.Select(item => new { Name = item.Name, Age = item.Age }).ToList();
3. 排序(Sorting)
使用 OrderBy
或 OrderByDescending
方法对集合进行排序。
var sortedList = list.OrderBy(item => item.Name).ToList();
var sortedListDesc = list.OrderByDescending(item => item.Age).ToList();
4. 分组(Grouping)
使用 GroupBy
方法根据一个或多个键将集合中的元素分组。
var grouped = list.GroupBy(item => item.Category).ToList();
5. 聚合(Aggregation)
使用 Count
, Sum
, Average
, Min
, Max
等方法进行聚合计算。
var count = list.Count();
var total = list.Sum(item => item.Value);
var average = list.Average(item => item.Age);
var min = list.Min(item => item.Value);
var max = list.Max(item => item.Age);
6. 查找(Finding)
使用 First
, FirstOrDefault
, Single
, SingleOrDefault
, Any
, All
等方法查找或检查集合中的元素。
var firstItem = list.First();
var firstItemMatching = list.FirstOrDefault(item => item.Name == "John");
var anyMatching = list.Any(item => item.Value > 100);
var allMatching = list.All(item => item.Active);
7. 连接(Joining)
使用 Join
或 GroupJoin
方法来合并两个集合中的数据,类似于 SQL 中的 JOIN 操作。
var query = from customer in customers
join order in orders on customer.CustomerID equals order.CustomerID
select new { CustomerName = customer.Name, OrderID = order.OrderID };
// 或者使用 GroupJoin
var groupedJoin = customers.GroupJoin(orders, c => c.CustomerID, o => o.CustomerID, (c, os) => new { Customer = c, Orders = os.ToList() });
8. 分页(Paging)
使用 Skip
和 Take
方法实现分页功能。
var page = list.Skip((pageNumber - 1) * pageSize).Take(pageSize).ToList();
9. 转换为其他类型
使用 ToList()
, ToArray()
, ToDictionary()
, ToLookup()
等方法将查询结果转换为不同的集合类型。
var dictionary = list.ToDictionary(item => item.Id, item => item.Name);
var lookup = list.ToLookup(item => item.Category);
10. 自定义扩展方法
通过定义自己的扩展方法,为任何类型添加额外的 LINQ 功能。
public static class MyExtensions
{
public static IEnumerable<T> MyCustomMethod<T>(this IEnumerable<T> source, Func<T, bool> condition)
{
// 自定义逻辑
return source.Where(condition); // 例如,这里只是包装了 Where 方法
}
}
11. 使用LINQ查询并返回前100条记录 (EF Core)
以下是一个使用Entity Framework Core(EF Core)作为示例,从数据库中返回前100条记录的例子。假设我们有一个名为DbContext
的类(EF Core的上下文类),它表示我们的数据库连接,并且我们有一个名为Blogs
的表(或对应的实体类)。
using (var context = new MyDbContext())
{
var top100Blogs = context.Blogs
.OrderBy(b => b.BlogId) // 假设我们按BlogId排序
.Take(100) // 取前100条记录
.ToList(); // 执行查询并返回结果列表
// 现在你可以遍历top100Blogs来查看结果
foreach (var blog in top100Blogs)
{
Console.WriteLine($"Blog ID: {blog.BlogId}, URL: {blog.Url}");
}
}
Nullable type
在C#中,Nullable类型允许变量存储其基础值类型的值,或者存储null
来表示没有值。这是C# 2.0引入的一个特性,它允许开发者为非引用类型(如int
、float
、struct
等)分配null
值。在C# 8.0及更高版本中,可空引用类型(nullable reference types)的引入进一步增强了这一特性,允许开发者指定引用类型(如string
、object
、类实例等)是否可以为null
。
可空值类型(Nullable Value Types)
对于值类型(如int
、double
等),在它们前面加上?
来创建它们的Nullable类型。例如,int?
表示一个可以为null
的整数。
int? nullableInt = 100; // 分配一个值
nullableInt = null; // 分配null
if (nullableInt.HasValue)
{
Console.WriteLine(nullableInt.Value); // 输出100
}
else
{
Console.WriteLine("nullableInt is null");
}
在上面的例子中,
nullableInt
是一个可以为null
的整数。使用HasValue
属性可以检查nullableInt
是否包含值,如果包含值,则可以通过Value
属性访问该值。
可空引用类型(Nullable Reference Types, C# 8.0及更高版本)
在C# 8.0及更高版本中,引入了可空引用类型的功能。这允许开发者在编译时更精确地控制引用类型的null
状态。默认情况下,在C# 8.0及更高版本的启用项目中,所有引用类型都被视为非可空的,但你可以通过在类型后面添加?
来显式声明它们可以为null
。
string? nullableString = "Hello World"; // 声明一个可以为null的字符串
nullableString = null; // 分配null
if (nullableString is not null)
{
Console.WriteLine(nullableString); // 输出Hello World
}
else
{
Console.WriteLine("nullableString is null");
}
在这个例子中,
nullableString
是一个可以为null
的字符串。C# 8.0及更高版本还引入了is not null
模式,用于更简洁地检查引用类型是否不为null
。
启用和禁用可空引用类型
在C#项目中,你可以通过修改项目文件(.csproj
)来启用或禁用可空引用类型。例如,要启用可空引用类型,你可以在.csproj
文件中添加以下元素:
<PropertyGroup>
<Nullable>enable</Nullable>
</PropertyGroup>
<!--要禁用可空引用类型(这将是C# 7.3及更低版本的默认行为),可以使用:-->
<PropertyGroup>
<Nullable>disable</Nullable>
</PropertyGroup>
然而,请注意,从C# 8.0开始,
enable
是默认设置,因此通常不需要显式启用它,除非你正在从早期版本的C#迁移项目并希望保持旧的行为。
Dynamic
在C#中,dynamic
关键字是一个静态类型,但在运行时它是动态的。使用 dynamic
类型的变量可以绕过编译时类型检查,其类型检查被延迟到运行时。这意味着你可以将 dynamic
类型的变量赋值为几乎任何类型的值,并且在编译时不会报错,因为编译器不会检查这些变量的类型兼容性。然而,如果在运行时尝试执行与变量实际类型不兼容的操作,则会抛出 RuntimeBinderException
异常。
dynamic
类型主要用于与动态语言(如Python、JavaScript等)的互操作性,以及在某些情况下简化代码,特别是当你不确定对象的确切类型,但你知道你将如何与之交互时。然而,过度使用 dynamic
可能会导致代码难以理解和维护,因为它减少了编译时类型检查的好处。
使用 dynamic
的例子1
假设你有一个返回 dynamic
类型对象的函数,这个函数可能返回不同类型的对象,具体取决于某些运行时条件:
public dynamic GetDynamicObject()
{
// 假设这里有一些逻辑来决定返回什么类型的对象
// 这里为了简单起见,我们直接返回一个字符串
return "Hello, Dynamic world!";
}
// 使用该函数
public void UseDynamicObject()
{
dynamic obj = GetDynamicObject();
// 这里编译器不会检查 obj 是否具有 Length 属性
// 如果在运行时 obj 实际上是一个字符串,那么这将正常工作
// 如果不是,将会抛出 RuntimeBinderException
Console.WriteLine(obj.Length); // 输出字符串的长度
// 你也可以调用方法,即使编译器不知道这个方法是否存在
// obj.SomeMethod(); // 如果 SomeMethod 在运行时对象的类型中存在,这将工作
}
使用 dynamic
的例子2
((dynamic)selectedItem).IsEnabled = false;
//verify whether existed the field
if (selectedItem.DynamicProperties.ContainsKey("isenabled "))
{
Console.WriteLine("existed");
}
注意事项
- 性能:使用
dynamic
可能会稍微影响性能,因为类型检查是在运行时进行的,而不是在编译时。 - 可读性和可维护性:过度使用
dynamic
会使代码更难理解和维护,因为它减少了编译时类型检查的好处。 - 错误处理:由于类型检查是在运行时进行的,因此错误(如类型不匹配)可能会在运行时而不是编译时发生,这可能会使调试更加困难。
因此,尽管 dynamic
在某些情况下很有用,但应该谨慎使用,并尽量在可以使用静态类型的地方使用静态类型。
Exception Handling
在C#中,异常处理是一种非常重要的编程实践,它允许你优雅地处理运行时错误,而不是让程序突然崩溃。C#通过try
、catch
和finally
块来支持异常处理。
基本结构
异常处理的基本结构如下:
try
{
// 尝试执行的代码块,可能会抛出异常
}
catch (ExceptionType1 ex)
{
// 当在try块中抛出ExceptionType1类型的异常时,执行这里的代码
}
catch (ExceptionType2 ex)
{
// 当在try块中抛出ExceptionType2类型的异常时,执行这里的代码
// 可以有多个catch块来处理不同类型的异常
}
finally
{
// 无论是否发生异常,finally块中的代码都会被执行
// 通常用于清理资源,如关闭文件、数据库连接等
}
抛出异常
你可以使用throw
关键字来抛出异常。如果抛出的异常是自定义的,你需要先定义一个继承自Exception
的类。
throw new Exception("发生了一个错误");
// 或者抛出自定义异常
throw new MyCustomException("发生了一个自定义错误");
捕获特定类型的异常
在catch
块中,你可以指定要捕获的异常类型。如果抛出的异常与catch
块中指定的类型不匹配,则执行下一个catch
块(如果有的话)。如果没有任何catch
块匹配,异常将被传递到调用堆栈中的上一个方法。
使用finally
块
finally
块是可选的,但它非常有用,因为它无论是否发生异常都会执行。这使其成为执行清理代码(如关闭文件流或数据库连接)的理想位置。
异常过滤
从C# 6.0开始,你可以在catch
块中添加一个条件表达式(也称为异常过滤器),以决定是否捕获特定的异常。
try
{
// 尝试执行的代码
}
catch (Exception ex) when (ex.Message.Contains("特定条件"))
{
// 仅当异常消息包含"特定条件"时,才执行这里的代码
}
注意事项
- 尽量避免在
catch
块中吞没异常(即捕获异常后不进行任何处理或记录)。这会使调试和错误跟踪变得更加困难。 - 尽可能具体地捕获异常类型,而不是总是捕获
Exception
类型。这有助于你更准确地了解发生了什么问题,并相应地处理它。 - 在可能的情况下,考虑将异常信息记录到日志中,以便稍后进行问题诊断。
- 谨慎使用
finally
块中的代码,确保它不会抛出新的异常,因为这可能会掩盖原始异常。如果finally
块中的代码确实可能抛出异常,请考虑在try
块中捕获并处理它,或者确保finally
块中的代码足够健壮,能够处理潜在的异常。
async/await
C# 中的 async
和 await
关键字是 C# 5.0 引入的,它们用于简化异步编程模型。这些关键字使得编写异步代码变得更为直观和易于理解,同时避免了传统的回调和事件处理模型的复杂性。
基本概念
-
async:这是一个修饰符,用于标记一个方法、lambda 表达式或匿名方法是异步的。在方法声明中使用
async
关键字会允许你在该方法的体内使用await
关键字。async
方法会隐式地返回一个Task
或Task<T>
对象,其中T
是方法的返回类型。如果方法没有返回值,则返回Task
;如果有返回值,则返回Task<T>
。 -
await:这个关键字用于等待异步操作的完成。它只能用在被
async
修饰的方法内部。await
表达式会挂起async
方法的执行,直到等待的异步操作完成,然后继续执行async
方法中await
表达式后面的代码。在等待过程中,async
方法会立即返回其Task
或Task<T>
对象,而不会阻塞调用线程。
使用示例
假设我们有一个异步的 HTTP 请求方法,我们想要调用它并等待其完成:
public async Task<string> GetWebPageAsync(string url)
{
using (HttpClient client = new HttpClient())
{
HttpResponseMessage response = await client.GetAsync(url); // 等待异步操作完成
if (response.IsSuccessStatusCode)
{
string content = await response.Content.ReadAsStringAsync(); // 再次等待异步操作完成
return content;
}
throw new HttpRequestException("Request failed with status code: " + response.StatusCode);
}
}
public async Task UseWebPageAsync()
{
string webPage = await GetWebPageAsync("https://example.com"); // 等待 GetWebPageAsync 完成
Console.WriteLine(webPage);
}
在这个例子中,
GetWebPageAsync
是一个异步方法,它使用HttpClient
来发送 HTTP GET 请求并获取网页内容。await client.GetAsync(url);
等待 HTTP 请求的完成,而不会阻塞调用线程。同样,await response.Content.ReadAsStringAsync();
等待响应内容的读取完成。
UseWebPageAsync
方法也是异步的,它调用 GetWebPageAsync
并等待其完成,然后打印返回的网页内容。
- 异步方法通常包含至少一个
await
表达式。如果一个方法被标记为async
但不包含await
表达式,编译器将发出警告,因为该方法的异步性可能不是必需的。 await
只能在async
方法内部使用。- 使用
await
时,当前方法的执行会在await
表达式处暂停,直到等待的异步操作完成,然后继续执行await
表达式后面的代码。但是,调用该方法的线程(通常是 UI 线程或 ASP.NET 上下文中的请求线程)不会被阻塞。 - 异步方法返回
Task
或Task<T>
,这允许调用者使用await
等待异步操作的完成,或者使用.Result
、.GetAwaiter().GetResult()
(不推荐,因为它们可能导致死锁)或其他方法来同步地等待结果。但是,通常建议尽可能使用await
来保持代码的异步性。