1. 属性:C#中的智能字段封装
在C#中,属性(Property)是字段(Field)的智能进化版。它看起来像字段,用起来像字段,但实际上是两个方法的语法糖——get访问器和set访问器。这种设计完美体现了面向对象编程的封装原则。
1.1 基础属性实现
最简单的属性声明如下:
csharp复制private string _name; // 私有字段
public string Name // 公共属性
{
get { return _name; }
set { _name = value; }
}
这种模式虽然看起来多此一举,但它带来了关键优势:
- 可以在setter中添加验证逻辑
- 可以在getter中实现延迟加载
- 未来可以修改内部实现而不影响外部调用
1.2 自动实现的属性
C# 3.0引入了自动属性语法糖:
csharp复制public string Name { get; set; } // 编译器会自动生成私有字段
这种写法简洁,但要注意:
- 不能直接访问后台字段
- 初始化需要使用属性初始化器:
csharp复制public string Name { get; set; } = "Unknown";
1.3 属性访问控制
可以单独控制get和set的访问级别:
csharp复制public string Name { get; private set; } // 外部只读
这种设计常用于:
- 希望外部只读,但类内部可以修改的场景
- 防止集合被意外替换(但元素仍可修改)
重要提示:属性虽然用起来像字段,但本质是方法。频繁调用的高性能场景应考虑直接使用字段。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 高级属性技巧
2.1 计算属性
属性不一定要有后台字段,可以完全基于计算:
csharp复制public class Rectangle
{
public double Width { get; set; }
public double Height { get; set; }
public double Area => Width * Height; // 只读计算属性
}
2.2 延迟初始化属性
使用Lazy
csharp复制private Lazy<ExpensiveObject> _expensive = new Lazy<ExpensiveObject>();
public ExpensiveObject Expensive => _expensive.Value;
2.3 属性变更通知
实现INotifyPropertyChanged接口,用于数据绑定:
csharp复制public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string name = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
private string _name;
public string Name
{
get => _name;
set
{
if (_name != value)
{
_name = value;
OnPropertyChanged();
}
}
}
3. 索引器:让对象像数组一样使用
索引器(Indexer)是C#中一种特殊属性,允许对象像数组一样通过索引访问。它的声明语法类似属性,但使用this关键字和方括号。
3.1 基本索引器实现
csharp复制public class StringArray
{
private string[] _array = new string[100];
public string this[int index]
{
get => _array[index];
set => _array[index] = value;
}
}
// 使用
var sa = new StringArray();
sa[0] = "Hello";
Console.WriteLine(sa[0]);
3.2 多参数索引器
索引器可以接受多个参数,实现多维访问:
csharp复制public class Matrix
{
private double[,] _data = new double[10,10];
public double this[int row, int col]
{
get => _data[row, col];
set => _data[row, col] = value;
}
}
3.3 字符串索引器
索引器参数不限于整数:
csharp复制public class Config
{
private Dictionary<string, string> _settings = new();
public string this[string key]
{
get => _settings.TryGetValue(key, out var value) ? value : null;
set => _settings[key] = value;
}
}
4. 索引器高级应用
4.1 复合索引器
结合多种参数类型创建灵活API:
csharp复制public class StudentCollection
{
private List<Student> _students = new();
// 通过ID索引
public Student this[int id] => _students.FirstOrDefault(s => s.Id == id);
// 通过姓名索引
public Student this[string name] => _students.FirstOrDefault(s => s.Name == name);
}
4.2 只读索引器
可以创建只读索引器:
csharp复制public class ImmutableArray<T>
{
private readonly T[] _items;
public ImmutableArray(IEnumerable<T> items)
{
_items = items.ToArray();
}
public T this[int index] => _items[index];
}
4.3 索引器性能优化
对于高频访问场景,可以优化索引器:
csharp复制public class HighPerformanceCollection
{
private struct Entry
{
public int HashCode;
public int Next;
public string Key;
public string Value;
}
private Entry[] _entries;
public string this[string key]
{
get
{
var hashCode = key.GetHashCode();
var index = hashCode % _entries.Length;
while (true)
{
ref var entry = ref _entries[index];
if (entry.HashCode == hashCode && entry.Key == key)
return entry.Value;
if (entry.Next == -1)
return null;
index = entry.Next;
}
}
}
}
5. 属性与索引器的实战应用
5.1 配置系统实现
利用索引器构建灵活配置系统:
csharp复制public class AppConfig
{
private readonly IConfiguration _config;
public AppConfig(IConfiguration config)
{
_config = config;
}
public string this[string key]
{
get => _config[key];
set => _config[key] = value;
}
// 强类型属性
public int Port => int.Parse(this["port"] ?? "8080");
public string Env => this["environment"] ?? "Development";
}
5.2 数据绑定示例
在WPF中实现完整的数据绑定:
csharp复制public class Product : INotifyPropertyChanged
{
private string _name;
private decimal _price;
public string Name
{
get => _name;
set
{
if (_name != value)
{
_name = value;
OnPropertyChanged();
}
}
}
public decimal Price
{
get => _price;
set
{
if (_price != value)
{
_price = value;
OnPropertyChanged();
OnPropertyChanged(nameof(FormattedPrice));
}
}
}
public string FormattedPrice => Price.ToString("C");
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
5.3 集合包装器
创建类型安全的集合包装器:
csharp复制public class SafeCollection<T>
{
private readonly List<T> _items = new();
private readonly object _lock = new();
public T this[int index]
{
get
{
lock (_lock)
{
return _items[index];
}
}
set
{
lock (_lock)
{
_items[index] = value;
}
}
}
public int Count
{
get
{
lock (_lock)
{
return _items.Count;
}
}
}
}
6. 常见问题与最佳实践
6.1 属性设计准则
- 保持简单:属性访问器不应有显著性能开销
- 避免异常:属性getter不应抛出异常
- 保持幂等:多次调用应返回相同结果
- 线程安全:如有必要,实现适当同步
6.2 索引器设计建议
- 明确用途:仅在类表示集合时使用索引器
- 参数验证:始终验证索引参数
- 文档完整:明确说明有效范围和行为
- 性能考虑:高频访问路径应优化
6.3 性能考量
属性和索引器都有方法调用的开销。在极端性能敏感的场景中:
csharp复制// 好的做法:高频访问时缓存委托
private static readonly Func<MyClass, string> NameGetter =
Expression.Lambda<Func<MyClass, string>>(
Expression.Property(
Expression.Parameter(typeof(MyClass)),
"Name"))
.Compile();
// 使用时
var name = NameGetter(instance);
6.4 版本控制建议
- 避免将自动属性改为计算属性(破坏二进制兼容性)
- 添加新属性比修改现有属性更安全
- 考虑将属性设为virtual以允许未来重写
在实际项目中,我曾遇到一个案例:一个自动属性在后期需要添加验证逻辑,但由于该属性已经被大量序列化,直接修改会导致兼容性问题。最终我们采用了新增属性+Obsolete标记的方式逐步迁移:
csharp复制[Obsolete("Use ValidatedName instead")]
public string Name { get; set; }
public string ValidatedName
{
get => Name;
set
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException("Name cannot be empty");
Name = value;
}
}
这个经验告诉我们:在设计公共API时,即使是简单的属性也要考虑未来的扩展性。
