一、默认属性为什么总让人头疼

在C#开发中,我们经常会遇到需要为类设置默认属性的情况。比如,你定义了一个User类,希望每个新创建的User对象都自动拥有某些默认值,比如IsActive=true或者CreatedAt=DateTime.Now。听起来很简单对吧?但实际操作时,你会发现事情没那么顺利。

举个例子,假设我们有一个简单的User类:

// C# 示例代码(技术栈:.NET 6)
public class User
{
    // 我们希望新用户默认是活跃状态
    public bool IsActive { get; set; } = true;  // C# 6.0 引入的自动属性初始化语法

    // 创建时间默认是当前时间
    public DateTime CreatedAt { get; set; } = DateTime.Now;

    // 用户名必须手动赋值,没有默认值
    public string Username { get; set; }
}

看起来没问题,但实际使用时,可能会遇到一些坑。比如,如果你用new User()创建对象,IsActiveCreatedAt确实会有默认值。但如果这个对象是通过反序列化(比如从JSON转换而来)生成的,这些默认值可能不会生效!

二、默认属性的几种实现方式

1. 自动属性初始化(C# 6.0+)

这是最简单的方式,直接在属性声明时赋值:

public int MaxRetryCount { get; set; } = 3;  // 默认重试3次

优点:代码简洁,一目了然。
缺点:不适用于所有场景(比如反序列化时可能失效)。

2. 构造函数初始化

更可靠的方式是在构造函数中设置默认值:

public class Order
{
    public string Status { get; set; }

    public Order()
    {
        Status = "Pending";  // 构造函数中设置默认值
    }
}

优点:无论对象如何创建(new或反序列化),默认值都会生效。
缺点:如果属性很多,构造函数会显得臃肿。

3. 使用默认值特性(DefaultValue)

.NET提供了DefaultValueAttribute,但要注意它不会自动赋值,只是给IDE或序列化器提示:

public class Product
{
    [DefaultValue(100)]  // 仅作为提示,不会实际赋值
    public int Stock { get; set; } = 100;  // 仍然需要手动初始化
}

适用场景:配合UI框架或序列化工具使用。

三、实战:解决JSON反序列化的默认值问题

假设我们从API接收JSON数据并反序列化为User对象:

string json = "{\"Username\":\"Alice\"}";  // 不包含IsActive和CreatedAt
var user = JsonSerializer.Deserialize<User>(json);

// 问题:user.IsActive 和 user.CreatedAt 会是默认值吗?

答案是:取决于序列化器的配置!在System.Text.Json中,默认不会调用构造函数。解决方案是配置JsonSerializerOptions

var options = new JsonSerializerOptions
{
    PropertyNameCaseInsensitive = true,
    DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
    // 关键设置:强制反序列化时使用默认值
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
    IncludeFields = true,  // 如果需要包含字段
};

// 重新反序列化
var user = JsonSerializer.Deserialize<User>(json, options);

四、高级技巧:动态默认值

有时候默认值需要动态计算。比如,订单过期时间默认是创建时间+7天:

public class Order
{
    public DateTime CreatedAt { get; set; } = DateTime.Now;
    public DateTime ExpiresAt => CreatedAt.AddDays(7);  // 动态计算属性
}

注意:这种动态属性不会被序列化,除非标记为[JsonPropertyName]

五、总结与最佳实践

  1. 简单场景:直接用自动属性初始化(public int Value { get; set; } = 10;)。
  2. 复杂场景:在构造函数中初始化,确保反序列化时也能生效。
  3. 动态默认值:使用计算属性或延迟初始化。
  4. 序列化注意:配置序列化器以确保默认值行为符合预期。