在c#中保存对象公共状态时,如果这些状态是对象固有且不可变的组成部分,应优先选择抽象类;如果状态是可选的、可插拔的行为特征,或者需要跨继承层级共享,则应选择接口配合属性或字段实现。抽象类允许直接定义受保护的字段来存储状态,子类可直接访问和修改,而接口仅能定义契约,无法直接包含状态字段(除非使用默认实现,但这通常用于行为而非纯粹的状态存储)。

抽象类如何直接管理受保护的状态字段
当多个派生类需要共享相同的内部状态,且该状态对基类逻辑至关重要时,抽象类提供了最直接的机制。通过在抽象类中声明`protected`或`private`字段,所有非密封的派生类都可以直接读写这些字段,无需暴露公共getter/setter,从而封装了状态的访问逻辑。这种方式避免了为每个状态属性编写样板代码,同时保证了状态的一致性。

using System;
public abstract class EntityBase
{
protected int _id;
protected string _name;
protected DateTime _createdAt;
public EntityBase(int id, string name)
{
_id = id;
_name = name;
_createdAt = DateTime.UtcNow;
}
public void PrintStatus()
{
Console.WriteLine($"ID: {_id}, Name: {_name}, Created: {_createdAt}");
}
}
public class User : EntityBase
{
private string _email;
public User(int id, string name, string email) : base(id, name)
{
_email = email;
}
public void UpdateEmail(string newEmail)
{
_email = newEmail;
}
}
public class Program
{
public static void Main()
{
var user = new User(1, "Alice", "alice@example.com");
user.PrintStatus();
user.UpdateEmail("newalice@example.com");
// 注意:这里不能直接访问_user.email,但可以通过方法间接操作
// 若需直接暴露,可在基类提供受保护的方法或属性
}
}
上述代码展示了`EntityBase`作为抽象类,直接持有`_id`、`_name`和`_createdAt`三个状态字段。`User`类继承自`EntityBase`,可以直接使用这些字段,同时拥有自己的私有字段`_email`。这种结构清晰地分离了共享状态和特定状态。
接口如何实现状态共享与多态行为
接口本身不包含状态字段,但可以通过属性定义器(get/set)来隐式地要求实现者提供状态支持。在C# 8.0及更高版本中,接口还可以提供默认实现,包括带有私有字段的属性,这使得接口也能在一定程度上管理状态。然而,这种方式更适用于定义行为契约,而非纯粹的状态容器。当需要多个不相关的类共享相同的状态访问模式时,接口提供了更大的灵活性,因为它允许多重实现。

using System;
public interface IIdentifiable
{
int Id { get; set; }
string Name { get; set; }
}
public interface ITrackable
{
DateTime CreatedAt { get; }
}
public class Product : IIdentifiable, ITrackable
{
public int Id { get; set; }
public string Name { get; set; }
public DateTime CreatedAt { get; private set; }
public Product(int id, string name)
{
Id = id;
Name = name;
CreatedAt = DateTime.UtcNow;
}
}
public class Order : IIdentifiable, ITrackable
{
public int Id { get; set; }
public string Name { get; set; } // 订单号
public DateTime CreatedAt { get; private set; }
public Order(int id, string orderNumber)
{
Id = id;
Name = orderNumber;
CreatedAt = DateTime.UtcNow;
}
}
public class StateManager
{
public void Process(IIdentifiable item)
{
Console.WriteLine($"Processing item with ID: {item.Id}, Name: {item.Name}");
}
}
public class Program
{
public static void Main()
{
var product = new Product(101, "Laptop");
var order = new Order(202, "ORD-12345");
var manager = new StateManager();
manager.Process(product);
manager.Process(order);
}
}
在这个例子中,`IIdentifiable`和`ITrackable`接口定义了状态访问的契约。`Product`和`Order`类实现了这些接口,各自维护自己的状态字段。`StateManager`类通过接口类型`IIdentifiable`来处理任何实现了该接口的对象,从而实现了对不同对象状态的统一访问。这种方式解耦了状态的具体实现与使用状态的业务逻辑。
当状态字段被意外修改时会发生什么
在抽象类中,如果状态字段被声明为`protected`,派生类可以直接修改它,这可能导致状态不一致。例如,如果`User`类直接修改了`_name`字段,而没有触发任何验证逻辑,可能会导致数据损坏。为了避免这种情况,可以在抽象类中提供受保护的虚方法来处理状态变更,或者将字段改为`private`并通过属性进行访问控制。
using System;
public abstract class SafeEntityBase
{
private int _id;
private string _name;
public int Id
{
get => _id;
protected set
{
if (value _name;
protected set
{
if (string.IsNullOrWhiteSpace(value)) throw new ArgumentException("Name cannot be empty");
_name = value;
}
}
public SafeEntityBase(int id, string name)
{
Id = id;
Name = name;
}
}
public class SafeUser : SafeEntityBase
{
public SafeUser(int id, string name) : base(id, name)
{
// 派生类不能直接访问_id或_name,必须通过属性
}
public void ResetName()
{
// 尝试直接赋值会编译错误,必须通过属性
// _name = "New Name"; // 错误
Name = "New Name"; // 正确,触发验证
}
}
public class Program
{
public static void Main()
{
try
{
var user = new SafeUser(-1, "Bob"); // 抛出异常
}
catch (ArgumentException ex)
{
Console.WriteLine(ex.Message); // 输出: ID must be non-negative
}
}
}
通过将字段设为`private`并提供`protected` setter的属性,可以确保状态变更经过验证。派生类无法绕过验证直接修改底层字段,从而提高了代码的健壮性。这种设计模式在需要严格状态管理的场景中非常有用。











