MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C#中的属性与索引器应用实践

2021-04-293.1k 阅读

C#属性基础

在C# 中,属性(Property)是一种特殊的成员,它结合了字段(Field)和方法(Method)的特点。从本质上讲,属性提供了一种灵活的机制来读取、写入或计算私有字段的值。属性允许以一种类似字段访问的语法来访问数据,同时在背后可以执行自定义的逻辑。

属性一般由访问器(Accessor)组成,主要有 getset 访问器。get 访问器用于返回属性的值,set 访问器用于设置属性的值。以下是一个简单的属性示例:

class Person
{
    private string name;

    public string Name
    {
        get { return name; }
        set { name = value; }
    }
}

在上述代码中,Person 类有一个私有字段 name,而 Name 属性为这个私有字段提供了公共的访问方式。get 访问器返回 name 字段的值,set 访问器则将传入的值赋给 name 字段。这里的 value 关键字是 set 访问器的隐式参数,代表要设置的新值。

我们可以这样使用这个属性:

class Program
{
    static void Main()
    {
        Person person = new Person();
        person.Name = "John";
        Console.WriteLine(person.Name);
    }
}

这段代码创建了一个 Person 对象,通过属性 Name 设置了名字为 "John",然后又通过 Name 属性读取并输出了这个名字。

自动实现的属性

从C# 3.0 开始,引入了自动实现的属性(Auto - implemented Properties)。这种属性允许我们在不声明对应的私有字段的情况下快速定义属性。编译器会自动为我们生成一个私有字段。以下是自动实现属性的示例:

class Employee
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

在上述代码中,Employee 类有两个自动实现的属性 FirstNameLastName。编译器会为每个属性生成一个对应的私有字段。使用方式如下:

class Program
{
    static void Main()
    {
        Employee employee = new Employee();
        employee.FirstName = "Jane";
        employee.LastName = "Doe";
        Console.WriteLine($"{employee.FirstName} {employee.LastName}");
    }
}

自动实现的属性非常方便,特别是在属性只需要简单的读取和写入操作时。但是,如果需要在 getset 访问器中添加额外的逻辑,就需要使用完整的属性定义方式。

只读和只写属性

  1. 只读属性 只读属性只有 get 访问器,没有 set 访问器。这意味着属性的值只能读取,不能在类外部设置。例如:
class Circle
{
    private double radius;

    public Circle(double r)
    {
        radius = r;
    }

    public double Area
    {
        get { return Math.PI * radius * radius; }
    }
}

Circle 类中,Area 属性是只读的,它根据 radius 字段计算圆的面积。外部代码只能读取 Area 属性的值,无法修改它。

class Program
{
    static void Main()
    {
        Circle circle = new Circle(5);
        Console.WriteLine(circle.Area);
    }
}
  1. 只写属性 只写属性只有 set 访问器,没有 get 访问器。虽然这种属性不太常见,但在某些特定场景下还是有用的,比如当我们只需要在类外部设置一个值,而不需要在外部读取它时。例如:
class Logger
{
    private string logFilePath;

    public string LogFilePath
    {
        set { logFilePath = value; }
    }

    public void LogMessage(string message)
    {
        if (!string.IsNullOrEmpty(logFilePath))
        {
            File.AppendAllText(logFilePath, message + Environment.NewLine);
        }
    }
}

Logger 类中,LogFilePath 属性是只写的。外部代码可以设置日志文件的路径,但不能直接读取这个路径。LogMessage 方法会根据设置的路径将日志消息写入文件。

class Program
{
    static void Main()
    {
        Logger logger = new Logger();
        logger.LogFilePath = "log.txt";
        logger.LogMessage("This is a test log message.");
    }
}

属性的访问修饰符

属性可以像方法和字段一样使用访问修饰符来控制访问级别。常见的访问修饰符有 publicprivateprotectedinternalprotected internal

  1. 公共属性(public 公共属性可以在任何地方访问,就像前面的大多数示例一样。例如:
class Product
{
    public string Name { get; set; }
    public decimal Price { get; set; }
}

Product 类的 NamePrice 属性都是公共的,任何代码都可以访问和修改它们的值。

  1. 私有属性(private 私有属性只能在类内部访问。这在某些情况下很有用,比如当我们有一个属性,它的逻辑只与类的内部实现相关,不应该被外部代码访问。例如:
class Order
{
    private decimal totalPrice;

    public decimal TotalPrice
    {
        get { return totalPrice; }
        private set { totalPrice = value; }
    }

    public void CalculateTotal()
    {
        // 假设这里有一些计算逻辑
        totalPrice = 100;
    }
}

Order 类中,TotalPrice 属性的 set 访问器是私有的。这意味着外部代码只能读取 TotalPrice 的值,不能直接设置它。CalculateTotal 方法可以在类内部设置 TotalPrice 的值。

  1. 受保护属性(protected 受保护属性可以在类本身以及它的派生类中访问。例如:
class Shape
{
    protected double area;

    public double Area
    {
        get { return area; }
    }
}

class Rectangle : Shape
{
    private double length;
    private double width;

    public Rectangle(double l, double w)
    {
        length = l;
        width = w;
        CalculateArea();
    }

    private void CalculateArea()
    {
        area = length * width;
    }
}

在这个例子中,Shape 类有一个受保护的 area 字段和一个公共的 Area 属性。Rectangle 类继承自 Shape 类,可以访问 Shape 类的 area 字段并在 CalculateArea 方法中设置它的值,然后通过 Area 属性提供给外部访问。

  1. 内部属性(internal 内部属性只能在定义它的程序集内部访问。如果我们有一个类库,希望某些属性只在库内部使用,而不暴露给其他引用该库的程序集,可以使用 internal 修饰符。例如:
// Assembly1.dll
internal class HelperClass
{
    internal string InternalProperty { get; set; }
}

在另一个程序集 Assembly2.exe 中引用 Assembly1.dllAssembly2.exe 的代码无法访问 HelperClassInternalProperty 属性,除非使用 InternalsVisibleTo 特性(这是一种特殊的设置,用于允许特定的其他程序集访问内部成员)。

  1. 受保护内部属性(protected internal 受保护内部属性结合了 protectedinternal 的特性。它可以在定义它的程序集内部以及其他程序集中的派生类中访问。例如:
// Assembly1.dll
public class BaseClass
{
    protected internal string ProtectedInternalProperty { get; set; }
}

// Assembly2.exe (which references Assembly1.dll)
public class DerivedClass : BaseClass
{
    public void SetValue()
    {
        ProtectedInternalProperty = "Some value";
    }
}

在这个例子中,DerivedClass 可以访问 BaseClassProtectedInternalProperty 属性,因为它是派生类。同时,在 Assembly1.dll 内部的其他类也可以访问这个属性。

静态属性

静态属性属于类本身,而不是类的实例。我们使用 static 关键字来定义静态属性。例如:

class MathUtils
{
    private static int counter = 0;

    public static int Counter
    {
        get { return counter; }
        set { counter = value; }
    }

    public static int Add(int a, int b)
    {
        counter++;
        return a + b;
    }
}

MathUtils 类中,Counter 是一个静态属性,用于记录 Add 方法被调用的次数。我们可以这样使用静态属性:

class Program
{
    static void Main()
    {
        Console.WriteLine(MathUtils.Counter);
        MathUtils.Add(2, 3);
        Console.WriteLine(MathUtils.Counter);
    }
}

注意,静态属性是通过类名来访问的,而不是通过类的实例。每个静态属性在整个应用程序域中只有一个实例,无论创建了多少个类的实例。

C#索引器基础

索引器(Indexer)允许一个对象可以像数组一样被索引。通过索引器,我们可以使用类似数组的语法来访问对象的成员。索引器不是一个独立的成员,它必须定义在类或结构体中。

索引器的定义与属性类似,不同的是索引器使用 this 关键字,并带有参数列表。以下是一个简单的索引器示例,用于一个自定义的字符串集合类:

class StringCollection
{
    private string[] items = new string[10];

    public string this[int index]
    {
        get
        {
            if (index >= 0 && index < items.Length)
            {
                return items[index];
            }
            return null;
        }
        set
        {
            if (index >= 0 && index < items.Length)
            {
                items[index] = value;
            }
        }
    }
}

StringCollection 类中,定义了一个索引器,它接受一个整数参数 indexget 访问器根据索引返回对应的字符串,如果索引越界则返回 nullset 访问器根据索引设置字符串的值,如果索引越界则不进行任何操作。

我们可以这样使用这个索引器:

class Program
{
    static void Main()
    {
        StringCollection collection = new StringCollection();
        collection[0] = "Apple";
        collection[1] = "Banana";
        Console.WriteLine(collection[0]);
        Console.WriteLine(collection[1]);
    }
}

上述代码创建了一个 StringCollection 对象,通过索引器设置了两个字符串,然后又通过索引器读取并输出了这两个字符串。

索引器的重载

索引器可以像方法一样进行重载,即定义多个不同参数列表的索引器。例如,我们可以为 StringCollection 类添加一个以字符串为索引的索引器,用于查找特定字符串在集合中的位置:

class StringCollection
{
    private string[] items = new string[10];

    public string this[int index]
    {
        get
        {
            if (index >= 0 && index < items.Length)
            {
                return items[index];
            }
            return null;
        }
        set
        {
            if (index >= 0 && index < items.Length)
            {
                items[index] = value;
            }
        }
    }

    public int this[string value]
    {
        get
        {
            for (int i = 0; i < items.Length; i++)
            {
                if (items[i] == value)
                {
                    return i;
                }
            }
            return -1;
        }
    }
}

在上述代码中,新增的索引器接受一个字符串参数 value,并返回该字符串在集合中的索引位置,如果找不到则返回 -1。我们可以这样使用这个重载的索引器:

class Program
{
    static void Main()
    {
        StringCollection collection = new StringCollection();
        collection[0] = "Apple";
        collection[1] = "Banana";
        Console.WriteLine(collection["Apple"]);
    }
}

这段代码通过以字符串为索引的索引器查找 "Apple" 在集合中的位置并输出。

索引器的访问修饰符

索引器也可以使用访问修饰符来控制访问级别,与属性类似,常见的有 publicprivateprotectedinternalprotected internal

  1. 公共索引器(public 公共索引器可以在任何地方访问,就像前面的示例一样。例如:
class DataStore
{
    private int[] data = new int[10];

    public int this[int index]
    {
        get { return data[index]; }
        set { data[index] = value; }
    }
}

DataStore 类的索引器是公共的,任何代码都可以通过索引来访问和修改 data 数组中的值。

  1. 私有索引器(private 私有索引器只能在类内部访问。这在某些情况下很有用,比如当我们有一个内部使用的索引器,不希望外部代码直接访问。例如:
class SecretData
{
    private int[] privateData = new int[5];

    private int this[int index]
    {
        get { return privateData[index]; }
        set { privateData[index] = value; }
    }

    public void ProcessData()
    {
        for (int i = 0; i < privateData.Length; i++)
        {
            this[i] = i * 2;
        }
    }
}

SecretData 类中,索引器是私有的。ProcessData 方法可以在类内部使用索引器来处理 privateData 数组,而外部代码无法直接访问这个索引器。

  1. 受保护索引器(protected 受保护索引器可以在类本身以及它的派生类中访问。例如:
class BaseArray
{
    protected int[] baseArray = new int[10];

    protected int this[int index]
    {
        get { return baseArray[index]; }
        set { baseArray[index] = value; }
    }
}

class DerivedArray : BaseArray
{
    public void FillArray()
    {
        for (int i = 0; i < baseArray.Length; i++)
        {
            this[i] = i + 1;
        }
    }
}

在这个例子中,BaseArray 类有一个受保护的索引器。DerivedArray 类继承自 BaseArray 类,可以在 FillArray 方法中使用这个受保护的索引器来填充数组。

  1. 内部索引器(internal 内部索引器只能在定义它的程序集内部访问。例如:
// Assembly1.dll
internal class InternalIndexerClass
{
    private string[] internalData = new string[5];

    internal string this[int index]
    {
        get { return internalData[index]; }
        set { internalData[index] = value; }
    }
}

在另一个程序集 Assembly2.exe 中引用 Assembly1.dllAssembly2.exe 的代码无法访问 InternalIndexerClass 的索引器,除非使用 InternalsVisibleTo 特性。

  1. 受保护内部索引器(protected internal 受保护内部索引器结合了 protectedinternal 的特性。它可以在定义它的程序集内部以及其他程序集中的派生类中访问。例如:
// Assembly1.dll
public class BaseIndexer
{
    protected internal int[] protectedInternalArray = new int[10];

    protected internal int this[int index]
    {
        get { return protectedInternalArray[index]; }
        set { protectedInternalArray[index] = value; }
    }
}

// Assembly2.exe (which references Assembly1.dll)
public class DerivedIndexer : BaseIndexer
{
    public void ModifyArray()
    {
        for (int i = 0; i < protectedInternalArray.Length; i++)
        {
            this[i] = i * 10;
        }
    }
}

在这个例子中,DerivedIndexer 类可以访问 BaseIndexer 类的受保护内部索引器,因为它是派生类。同时,在 Assembly1.dll 内部的其他类也可以访问这个索引器。

索引器与属性的比较

  1. 语法差异

    • 属性:属性使用属性名来访问,例如 obj.PropertyName。属性可以有 getset 访问器,并且通常用于访问对象的某个特定数据成员。
    • 索引器:索引器使用 this 关键字,通过类似数组的索引语法 obj[index] 来访问。索引器必须带有参数列表,参数可以是任何类型,常用于以某种方式索引对象内部的集合或数据结构。
  2. 用途差异

    • 属性:主要用于封装对象的状态数据,提供对类的字段的安全访问。属性可以包含复杂的逻辑,比如在设置值时进行验证,或者在获取值时进行计算。
    • 索引器:更侧重于提供一种方便的方式来访问对象内部的数据集合,就像访问数组一样。索引器可以根据不同的索引类型(如整数、字符串等)来访问和操作集合中的元素。
  3. 定义差异

    • 属性:属性的定义可以是自动实现的,也可以是完整定义的,包含 getset 访问器。属性可以有各种访问修饰符,还可以是静态的。
    • 索引器:索引器必须定义在类或结构体中,不能是静态的。索引器可以重载,通过不同的参数列表来提供不同的索引方式。索引器也可以有访问修饰符,但不能是自动实现的,必须包含 getset 访问器(可以只有其中一个,形成只读或只写索引器)。

复杂场景下属性与索引器的应用

  1. 属性在数据验证中的应用 在实际应用中,属性经常用于数据验证。例如,我们有一个表示人的年龄的属性,年龄必须在合理的范围内。
class Person
{
    private int age;

    public int Age
    {
        get { return age; }
        set
        {
            if (value >= 0 && value <= 120)
            {
                age = value;
            }
            else
            {
                throw new ArgumentOutOfRangeException(nameof(value), "Age must be between 0 and 120.");
            }
        }
    }
}

在这个例子中,Age 属性的 set 访问器对传入的年龄值进行了验证。如果年龄值不在 0 到 120 之间,就会抛出一个异常。这样可以确保 Person 对象的 Age 属性始终保持在合理的范围内。

  1. 索引器在多维数据结构中的应用 索引器在处理多维数据结构时非常有用。例如,我们可以定义一个二维数组的索引器来方便地访问二维数组中的元素。
class Matrix
{
    private int[,] matrixData;

    public Matrix(int rows, int cols)
    {
        matrixData = new int[rows, cols];
    }

    public int this[int row, int col]
    {
        get { return matrixData[row, col]; }
        set { matrixData[row, col] = value; }
    }
}

Matrix 类中,定义了一个接受两个整数参数的索引器,分别表示行和列。通过这个索引器,我们可以像访问二维数组一样方便地访问和修改 matrixData 中的元素。

class Program
{
    static void Main()
    {
        Matrix matrix = new Matrix(3, 3);
        matrix[0, 0] = 1;
        matrix[0, 1] = 2;
        matrix[1, 0] = 3;
        Console.WriteLine(matrix[0, 0]);
        Console.WriteLine(matrix[0, 1]);
        Console.WriteLine(matrix[1, 0]);
    }
}
  1. 属性和索引器在数据绑定中的应用 在 Windows Forms 或 WPF 等应用程序开发中,属性和索引器常用于数据绑定。例如,我们有一个包含学生成绩的类,并且希望在 UI 中显示这些成绩。
class Student
{
    private string name;
    private int[] scores;

    public Student(string n, int[] s)
    {
        name = n;
        scores = s;
    }

    public string Name
    {
        get { return name; }
    }

    public int this[int index]
    {
        get { return scores[index]; }
        set { scores[index] = value; }
    }
}

在 Windows Forms 中,我们可以将 Student 对象的 Name 属性和索引器绑定到 UI 控件上,以便用户可以查看和修改学生的姓名和成绩。具体的数据绑定代码会根据不同的 UI 框架有所不同,但基本原理是通过属性和索引器将数据与 UI 元素关联起来。

性能考虑

  1. 属性的性能 一般来说,简单的属性(自动实现的属性或只包含简单读取和写入操作的属性)在性能上与直接访问字段几乎没有区别。编译器在优化时会将简单属性的访问转换为类似字段访问的操作。但是,如果属性的 getset 访问器包含复杂的逻辑,如大量的计算、文件 I/O 或数据库访问等,那么属性的访问性能就会受到影响。在这种情况下,需要权衡属性提供的便利性和性能开销。

  2. 索引器的性能 索引器的性能也取决于其实现。如果索引器只是简单地访问内部数组或集合中的元素,性能通常较好。然而,如果索引器在 getset 访问器中执行复杂的查找逻辑,比如在大型集合中进行线性查找,性能可能会显著下降。对于频繁访问的索引器,应该尽量优化其内部实现,例如使用更高效的数据结构(如哈希表)来提高查找速度。

在实际开发中,需要根据具体的应用场景和性能需求来合理设计属性和索引器。如果性能是关键因素,可能需要进行性能测试和优化,以确保属性和索引器的操作不会成为应用程序的性能瓶颈。

与其他编程语言的对比

  1. 与 Java 的对比
    • 属性:Java 中没有像 C# 那样直接的属性概念。在 Java 中,通常使用 getter 和 setter 方法来实现类似属性的功能。例如,在 Java 中定义一个表示人的名字的属性,代码如下:
public class Person {
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

而在 C# 中,可以使用属性来实现相同的功能,语法更加简洁:

class Person
{
    public string Name { get; set; }
}
- **索引器**:Java 中没有索引器的概念。在 Java 中,如果要实现类似通过索引访问集合元素的功能,通常使用数组或实现 `List` 等接口。而 C# 的索引器提供了一种更灵活的方式,可以根据不同的索引类型来访问对象内部的数据集合。

2. 与 Python 的对比 - 属性:Python 中可以使用 @property 装饰器来实现类似属性的功能。例如:

class Person:
    def __init__(self):
        self._name = None

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, value):
        self._name = value

与 C# 相比,Python 的属性实现方式更加灵活,但语法上略有不同。C# 的属性语法更接近传统的字段访问语法,而 Python 的属性通过装饰器来定义,更强调函数式编程的风格。 - 索引器:Python 中没有直接的索引器概念。Python 的列表和字典等数据结构通过 __getitem____setitem__ 方法来实现索引访问。例如,对于一个自定义的类,如果要支持索引访问,可以这样实现:

class MyList:
    def __init__(self):
        self.data = []

    def __getitem__(self, index):
        return self.data[index]

    def __setitem__(self, index, value):
        self.data[index] = value

C# 的索引器在语法上更加简洁和直观,直接使用 this 关键字来定义,而 Python 的这种实现方式更偏向于魔法方法的概念。

通过与其他编程语言的对比,可以更好地理解 C# 中属性和索引器的特点和优势,以及在不同编程环境下如何选择合适的方式来实现类似的功能。