[CLR via C#]10. 属性

简介: 原文:[CLR via C#]10. 属性一、无参属性   对于字段,强烈建议将所有的字段都设为private。如果允许用户或类型获取或设置状态信息,就公开一个针对该用途的方法。封装了字段访问的方法通常称为访问器(accessor)方法。
原文: [CLR via C#]10. 属性

一、无参属性

  对于字段,强烈建议将所有的字段都设为private。如果允许用户或类型获取或设置状态信息,就公开一个针对该用途的方法。封装了字段访问的方法通常称为访问器(accessor)方法。访问器方法可选择对数据的合理性进行检查,确保对象的状态永远不被破坏。如下代码:

    private sealed class Employee
    {
        private String m_Name;
        private Int32 m_Age;
 
        public String GetName(){
            return m_Name;
        }
 
        public void SetName(String value){
            m_Name = value;
        }
 
        public Int32 GetAge(){
            return m_Age;
        }
 
        public void SetAge(Int32 value){
            if (value <= 0)
                throw new ArgumentOutOfRangeException("value", "must be >0");
            m_Age = value;
        }
    }

  想这样的数据封装有两个缺点。第一:不得不实现额外的方法;第二、用户必须调用方法。

  CLR提供了属性(Property)的机制,它缓解了第一个缺点所造成的的影响,同事完全消除了第二个缺点。如下面代码:
   private sealed class Employee {
        private String m_Name; 
        private Int32 m_Age;  
 
        public String Name {
            get { return (m_Name); }
            set { m_Name = value; } // 关键字'value' 总是代表新值
        }
 
        public Int32 Age {
            get { return (m_Age); }
            set {
                if (value <= 0)    // 关键字'value' 总是代表新值
                    throw new ArgumentOutOfRangeException("value", "must be >0");
                m_Age = value;
            }
        }
    }

  于是就可以这样调用:

  Employee emp = new Employee();
  emp.Name = "Jeffrey Richter";
  emp.Age = 45; 
  Console.WriteLine("Employee info: Name = {0}, Age = {1}", emp.Name, emp.Age);

  可将属性想象成智能字段,即背后有额外逻辑的字段。CLR支持静态、实例、抽象和虚属性。属性可以使用任意可访问性修饰符修饰。

  每个属性都有一个名称和一个类型(类型不能为void).属性不能重载。也就是说不能定义名称相同,类型不同的属性。 定义属性时,通常要同时指定get和set两个方法。但是,可以省略set方法来定义一个只读属性,或者省略get方法定义一个只写属性。
 
  定义属性时,取决属性的定义,编译器在最后的托管程序集中生成以下两项或三项:
  *)代表属性的get访问器方法的一个方法。仅在属性定义了get访问器方法时生成。
  *) 代表属性的set访问器方法的一个方法。仅在属性定义了set访问器方法时生成。
  *)托管程序集元数据中的一个属性定义。这一项是肯定要生成的。
 
  以前面的Employee类为例,编译器将会生成4个方法定义。如图:
  
  编译器在你指定的属性名之前会附加get_或set_前缀,从而自动生成这些方法的名称。C#内建了对属性的支持。当编译器发现代码试图获取或设置一个属性时,它实际会生成对上述某个方法的一个调用。
  1. 自动实现属性
  如果只是为了封装一个私有字段而创建一个属性,C#还提供了一种更简单的语法,称为自动实现的属性(Automatically implemented Property,AIP)。下面是Name属性的一个例子:
    private sealed class Employee {
        //这是一个自动实现属性
        public String Name {get;set;}
    }

  2.合理定义属性

  属性和字段的比较:

  1) 属性可以是只读或只写的,字段访问确总是可读和可写。如果定义一个属性,最好同时为它提供get和set访问器方法。
  2) 一个属性方法可能抛出异常;字段访问永远不会抛出异常。
  3) 属性不能作为out或ref参数传给方法;字段却可以。
  4)属性方法可能花费较长时间执行;字段的访问总是立即完成的。
  5)如果连续多次调用,属性方法每次都可能返回一个不同的值;而字段每次调用都返回相同的值。
  6)属性方法可能造成明显的side effect(指访问属性时,除了单纯的设置或获取属性,还会造成对象状态的改变);字段访问永远不会。
  7)属性方法可能需要额外的内存,或者返回一个不正确的引用,指向不属于对象状态一部分的某个东西,这样一来,对返回对象的修改就作用不到原始对象身上了。相反,查询字段返回的总是正确的引用,它指向的东西保证是原始对象状态的一部分。
 
  现在的开发人员对属性的依赖有过之而无不及,经常有没有必要都使用属性,仔细看下上面的比较,你会发现在极少数的情况下,才有必要定义属性。属性的唯一好处就是提供了简化的语法,和调用普通方法相比,属性不仅不会提高代码性能,还会妨碍对代码的理解。建议就是让开发人员老老实实的写Getxxx和Setxxx方法,希望编译器提供一种特殊的,简化的,有别于字段访问的语法,是开发人员知道他们实际上是在调用一个方法。
 
  3.对象和集合初始化器
  我们经常要构造一个对象,然后设置对象的一些公共属性(或字段)。为了简化这个常见的编程模式,C#语言支持一种特殊的对象初始化语法。比如:[()]代表"()"可要可不要。
Employee e = new Employee[()] { Name = "Jeff", Age = 45 }
  对象初始化器语法真正的好处在于,它允许在表达式的上下文(相对于语句的上下文)中编码,允许组合多个函数,进而增强了代码的可读性。于是,就可以这么写了:
string s = new Employee() {Name = "Jeff", Age = 45}.ToString().ToUpper();
  
  4.匿名类型
  利用C#的匿名类型,可以使用非常简洁的语法来声明一个不可变的元组类型。元组(Tuple)类型是含有一组属性的类型,这些属性通常以某种方式相互关联。
//定义一个类型,后再它的一个实例,并初始化它的属性
var o1 = new { Name = "Jeff", Year = 1964 };
Console.WriteLine("Name={0}, Year={1}", o1.Name, o1.Year);

  第一行代码创建了一个匿名类型,没有在new 关键字后制定类型名称,所以编译器会为我自动创建一个类型名称,而且不会告诉我这个名称是什么(这正是匿名类型一词的由来),但编译器是知道的。虽然我不知道变量o1声明的是什么类型,但可以利用C#的"隐式推断类型局部变量"功能(var)。

  编译器支持用另外两种语法声明匿名类型中的属性,它根据变量推断出属性名和类型:

String Name = "Grant";
DateTime dt = DateTime.Now;

// 有两个属性的一个匿名类型
// 1. String Name 属性设为"Grant"
// 2. Int32 Year 属性设为dt中的年份
var o2 = new { Name, dt.Year };

  在这个例子中,编译器判断第一个属性名为Name。由于Name是一个局部变量的名称,所以编译器将属性类型设为与局部变量相同的类型:String。对于第二个属性,编译器使用字段/属性的名称:Year。Year是DateTime类的一个Int32属性,所以匿名类型中的Year属性也是一个Int32。

  如果编译器看见你在源代码中定义了多个匿名类型,而且这些类型具有相同的结构,那么它只会创建一个匿名类型定义,但可以创建该类型的多个实例。相同的结构,指在这些匿名类型中,每个属性都有相同的类型和名称,而且这些属性的指定顺序相同。
   匿名类型经常和LINQ技术配合使用。可用LINQ进行查询,从而生成由一组对象构成的集合,这些对象都是相同的匿名类型。然后,可以对结果集中的对象进行处理。所有的这些都在一个方法中完成。
  5.System.Tuple类型
  在System命名空间,Microsoft定义了几个泛型Tuple(元组)类型。它们全部从Object派生,区别只在于元数(泛型参数的个数)。
  在计算机编程中,一个函数或运算的元数是指函数获取的实参或操作数的个数。
//这是最简单的
public class Tuple<T1> {
    private T1 m_item1;
    public Tuple(T1 item1) { m_Item1 = item1;}
    public item1 { get { retuen m_Item1; } }
}

  和匿名类型相似,一旦创建好了一个Tuple,他就不可变了(所有属性都只读)。Tuple类还提供了CompareTo,Equals,GetHashCode和ToString方法,另外还提供了一个Size属性。除此之外,所有Tuple类型都实现了IstruralEquatable,IstructuralComparable和IComparable接口,所以可以比较两个Tuple对象。

 

二、有参属性
 
  编程语言还支持所谓的有参属性,它的get访问器方法接收一个或多个属性,set访问器方法接收两个或多个参数。 C#语言把它们称为索引器。VB称为默认属性。
  C#使用数组风格的语法来公开有参属性(索引器)。换句话说,可将索引器看作C#开发人员重载[]操作符的一种方式。下面是一个实例BitArray类,它允许用数组风格的语法来索引由该类的一个实例维护的一组二进制位。
internal sealed class BitArray {
    // 容纳了二进制位的私有字节数组
    private Byte[] m_byteArray;
    private Int32 m_numBits;
 
    // 下面的构造器用于分配字节数组,并将所有位设为 0
    public BitArray(Int32 numBits) {
        // 先验证实参
        if (numBits <= 0)
            throw new ArgumentOutOfRangeException("numBits must be > 0");
 
        // 保留位的个数
        m_numBits = numBits;
 
        // 为位数组分配字节
        m_byteArray = new Byte[(m_numBits + 7) / 8];
    }
 
 
    // 下面是索引器(有参属性)
    public Boolean this[Int32 bitPos] {
 
        // 下面是索引器的get访问器方法
        get {
            // 先验证实参
            if ((bitPos < 0) || (bitPos >= m_numBits))
                throw new ArgumentOutOfRangeException("bitPos", "bitPos must be between 0 and " + m_numBits);
 
            // 返回指定索引处的位的状态
            return ((m_byteArray[bitPos / 8] & (1 << (bitPos % 8))) != 0);
        }
 
        // 下面是索引器的set访问器方法
        set {
            if ((bitPos < 0) || (bitPos >= m_numBits))
                throw new ArgumentOutOfRangeException("bitPos", "bitPos must be between 0 and " + m_numBits);
 
            if (value) {
                // 将指定索引处的位设为true
                m_byteArray[bitPos / 8] = (Byte)
                   (m_byteArray[bitPos / 8] | (1 << (bitPos % 8)));
            } else {
                // 将指定索引处的位设为false
                m_byteArray[bitPos / 8] = (Byte)
                   (m_byteArray[bitPos / 8] & ~(1 << (bitPos % 8)));
            }
        }
    }
}

  BitArray类的调用也非常简单:

  
private static void BitArrayTest() {
        // 分配含有14个位的bitArray数组
        BitArray ba = new BitArray(14);
 
        // 调用set访问器方法,将编号为偶数的所有为设为true
        for (Int32 x = 0; x < 14; x++) {
            ba[x] = (x % 2 == 0);
        }
 
        // 调用get访问器方法显示所有为的状态
        for (Int32 x = 0; x < 14; x++) {
            Console.WriteLine("Bit " + x + " is " + (ba[x] ? "On" : "Off"));
        }
    }

 

  CLR本身并不区分无参属性和有参属性。对CLR来说,每个属性都只是类型中定义的一对方法和一些元数据。将this[...]作为表达一个索引器的语法,这纯粹是C#团队自己的选择,正因如此,C#只允许在对象的实例上定义索引器, C#没有提供定义静态索引器属性的语法,虽然CLR是支持静态有参属性的。
 

三、调用属性访问器方法时的性能

  对于简单的get和set访问器方法,JIT编译器会将代码内联。这样一来,使用属性(而不使用字段)就没有性能上的损失。
  内联是指将一个方法的代码直接编译到它的方法中。这样能避免在运行时发出调用所产生的开销,代价是编译好的方法的额代码会变得更大。
  由于属性访问器方法通常只包含及少量代码,所以对它们进行内联,反而会使最终生成的本地代码更小,执行更快。
  JIT编译器在调试代码时不会内联属性,因为这会变得难以调试。

 

四、属性访问器的可访问性

  我们有时希望为get访问器方法指定一种可访问性,为set访问器方法指定另一种可访问性。如下:

public class SomeType {
       private String m_name;
       public String Name {
             get { return m_name;}
             protected set { m_name = value;}
   }
}

  定义一个属性时,如果两个访问器方法需要具有不同的可访问性,C#语法要求必须为属性本身指定限制最不大的那一种可访问性。然后,在两个访问器中,只能选择一个来应用限制较大的那一种可访问性。如前面例子中,属性本身声明为public,set访问器方法声明为protected(限制比public大)。

 

五、泛型属性访问器方法

既然属性本质是方法,而且C#和CLR允许方法是泛型的,但是C#不允许定义泛型属性。这从概念上讲不通。属性本用来表示一项可供查询的或设置的对象特征。从概念上讲,属性是不具有行为的。
目录
相关文章
|
3月前
|
存储 安全 编译器
C# 11.0中的泛型属性:类型安全的新篇章
【1月更文挑战第23天】C# 11.0引入了泛型属性的概念,这一新特性为开发者提供了更高级别的类型安全性和灵活性。本文将详细探讨C# 11.0中泛型属性的工作原理、使用场景以及它们对现有编程模式的改进。通过深入了解泛型属性,开发者将能够编写更加健壮、可维护的代码,并充分利用C#语言的最新发展。
|
1月前
|
Java C#
C#学习相关系列之多线程(七)---Task的相关属性用法
C#学习相关系列之多线程(七)---Task的相关属性用法
|
3月前
|
开发框架 .NET C#
C# 10.0中的扩展属性与模式匹配:深入解析
【1月更文挑战第20天】C# 10.0引入了众多新特性,其中扩展属性与模式匹配的结合为开发者提供了更强大、更灵活的类型检查和代码分支能力。通过这一特性,开发者可以在不修改原始类的情况下,为其添加新的行为,并在模式匹配中利用这些扩展属性进行更精细的控制。本文将详细探讨C# 10.0中扩展属性与模式匹配的工作原理、使用场景以及最佳实践,帮助读者更好地理解和应用这一新功能。
|
3月前
|
运维 编译器 C#
C# 9.0中的本地函数属性:深化函数级别的控制
【1月更文挑战第17天】C# 9.0引入了本地函数属性的概念,允许开发者在本地函数上应用属性,从而进一步细化对函数行为的控制。这一新特性不仅增强了代码的可读性和可维护性,还为函数级别的编程提供了更多的灵活性。本文将探讨C# 9.0中本地函数属性的用法、优势以及可能的应用场景,帮助读者更好地理解并应用这一新功能。
|
4月前
|
XML 存储 JSON
C# | 使用Json序列化对象时忽略只读的属性
将对象序列化成为Json字符串是一个使用频率非常高的功能。Json格式具有很高的可读性,同时相较于XML更节省空间。 在开发过程中经常会遇到需要保存配置的场景,比如将配置信息保存在配置类型的实例中,再将这个对象序列化成为Json字符串并保存。当需要加载配置时,则是读取Json格式的字符串再将其还原成配置对象。在序列化的过程中,默认会将所有公开的属性和字段都序列化进入Json字符串中,这其中也会包含只读的属性或字段,而只读的属性和字段在反序列化的过程中其实是无意义的,也就是说这一部分存储是多余的。 本文将讲解如何在执行Json序列化时,忽略掉那些只读的属性和字段。
54 0
C# | 使用Json序列化对象时忽略只读的属性
|
5月前
|
前端开发 C# 开发工具
Unity快手上手【熟悉unity编辑器,C#脚本控制组件一些属性之类的】
Unity快手上手【熟悉unity编辑器,C#脚本控制组件一些属性之类的】
105 0
|
8月前
|
存储 编译器 C#
《More effective C#》第二章 尽量采用隐式属性来表示可变的数据
《More effective C#》第二章 尽量采用隐式属性来表示可变的数据
|
9月前
|
C#
C#属性的get与set
C#属性的get与set
|
SQL NoSQL C#
基于C#的ArcEngine二次开发32:属性sql查询语句与IMap,ILayer,IFeatureLayer,IFeatureClass关系
基于C#的ArcEngine二次开发32:属性sql查询语句与IMap,ILayer,IFeatureLayer,IFeatureClass关系
基于C#的ArcEngine二次开发32:属性sql查询语句与IMap,ILayer,IFeatureLayer,IFeatureClass关系
|
编解码 C#
基于C#的ArcEngine二次开发教程(17):获取栅格属性的接口及代码实现
基于C#的ArcEngine二次开发教程(17):获取栅格属性的接口及代码实现
基于C#的ArcEngine二次开发教程(17):获取栅格属性的接口及代码实现