你可能已经听说过 C# 的强类型系统,或者它是微软对 Java 的回应。但今天,我们将深入探讨一些即使是经验丰富的开发者也可能不知道的 C# 奇妙特性。
让我们一起来探索这门复杂语言中一些令人惊叹的特性,它们可能会改变你编写代码的方式。
default
操作符有一个秘密身份你是否注意到 default(T)
和 default
操作符并不完全相同?这里有一些有趣的东西:
var x = default; // 类型推断的魔法!
Console.WriteLine(x.GetType()); // 编译错误 - x 实际上是空的
int y = default;
Console.WriteLine(y); // 0
上下文关键字 default
比看起来更聪明。它从上下文中推断类型,使你的代码更简洁,同时保持类型安全。不需要显式的类型参数!
C# 属性有一个隐藏的特性:它们可以检测自己是否在构造函数中被使用。
public class TimeManager
{
private DateTime _createdAt;
public DateTime CreatedAt
{
get => _createdAt;
init
{
if (RuntimeHelpers.IsReferenceOrContainsReferences<TimeManager>())
_createdAt = value;
else
throw new InvalidOperationException("Time travel not allowed!");
}
}
}
init
访问器实际上可以检查它是否在对象初始化期间被调用。这使得一些非常复杂的不可变性模式成为可能。
虽然字符串是不可变的引用类型,但 C# 允许你像数组一样索引它们。但这里有一个转折:
string text = "Hello";
Console.WriteLine(text[1]); // 'e'
Span<char> span = text.AsSpan();
span[1] = 'a'; // 编译错误!
字符串实现了 IEnumerable<char>
并支持索引,但它们并不是真正的数组。AsSpan
方法通过防止修改揭示了它们的真实本质。
C# 有一种叫做“基于模式的固定语句”的东西,可以使处理非托管内存变得异常优雅:
public ref struct PinnedArray<T> where T : unmanaged
{
private Span<T> _span;
public ref T GetPinnableReference() => ref _span[0];
}
编译器识别这种模式,并启用固定语句支持,而无需任何不安全代码。这就像与编译器有一个秘密握手!
自 C# 8.0 以来,接口可以有静态成员,包括字段!但这里有一个陷阱:
public interface IProcessor
{
static readonly int MaxThreads = Environment.ProcessorCount;
static abstract void Initialize();
static virtual void Shutdown() => Console.WriteLine("Shutting down...");
}
你甚至可以在接口中拥有静态抽象成员。这就像接口在没人注意的时候获得了超能力。
值类型并不总是存储在栈上。看看这个:
struct LargeStruct
{
private byte[] data = new byte[1024];
}
LargeStruct local = new(); // 这个存储在哪里?
结构体本身可能在栈上,但它的 byte[]
字段却存储在堆上。栈和堆之间的界限比大多数开发者意识到的要微妙得多。
C# 的模式匹配非常强大,它可以查看尚未构造的对象的内部:
object obj = null;
if (obj is string { Length: > 5 } str)
{
Console.WriteLine(str);
}
模式匹配器会同时检查 null
、类型和属性条件,而不会抛出 NullReferenceException
。这就像薛定谔的模式匹配!
局部函数可以从它们的包含作用域中捕获变量,但有一个转折:
void ProcessData(int threshold)
{
int multiplier = 2;
local();
void local()
{
Console.WriteLine(threshold * multiplier);
}
multiplier = 3; // 这会影响局部函数!
}
它们可以看到并修改来自父作用域的变量,即使是在它们定义之后。这就像它们对周围上下文有 X 光透视能力。
记录不仅仅是具有自动属性生成的类:
public record Person(string Name, int Age)
{
public void Deconstruct(out string name, out int age) =>
(name, age) = (Name, Age);
}
它们自动实现了 IEquatable<T>
,支持解构,并通过编译器的巧妙魔法和运行时支持处理复制构造。
using
语句比你想象的更灵活using
语句不再仅仅用于 IDisposable
:
using var scope = new TransactionScope();
// 这里的代码
// 作用域在当前块结束时自动释放!
自 C# 8.0 以来,你可以使用简化的 using
声明,它会在作用域结束时自动释放。不再需要嵌套块!