C# 类型和成员基础以及常量、字段、属性

类型和成员基础

在C#中,一个类型内部可以定义多种成员:常量、字段、实例构造器、类型构造器(静态构造器)、方法、操作符重载、转换操作符、属性、事件、类型。

类型的可见性有public和internal(默认)两种,前者定义的类型对所有程序集中的所有类型都可见,后者定义的类型只对同一程序集内部的所有类型可见:

 public class PublicClass { }                //所有处可见
 internal class ExplicitlyInternalClass { }  //程序集内可见
 class ImplicitlyInternalClass { }           //程序集内可见(C#编译器默认设置为internal)

成员的可访问性(按限制从大到小排列):

  • Private只能由定义成员的类型或嵌套类型中方法访问
  • Protected只能由定义成员的类型或嵌套类型或派生类型中方法访问
  • Internal 只能由同程序集类型中方法访问
  • Protected Internal 只能由定义成员的类型或嵌套类型或派生类型或同程序集类型中方法访问(注意这里是或的关系)
  • Public 可由任何程序集中任何类型中方法访问

在C#中,如果没有显式声明成员的可访问性,编译器通常默认选择Private(限制最大的那个),CLR要求接口类型的所有成员都是Public访问性,C#编译器知道这一点,因此禁止显式指定接口成员的可访问性。同时C#还要求在继承过程中派生类重写成员时,不能更改成员的可访问性(CLR并没有作这个要求,CLR允许重写成员时放宽限制)。

静态类

永远不需要实例化的类,静态类中只能有静态成员。在C#中用static这个关键词定义一个静态类,但只能应用于class,不能应用于struct,因为CLR总是允许值类型实例化。

C#编译器对静态类作了如下限制:

  • 静态类必须直接从System.Object派生
  • 静态类不能实现任何接口(因为只有使用类的一个实例才能调用类的接口方法)
  • 静态类只能定义静态成员(字段、方法、属性、事件)
  • 静态类不能作为字段、方法参数或局部变量使用
  • 静态类在编译后,会生成一个被标记为abstract和sealed的类,同时编译器不会生成实例构造器(.ctor方法)

分部类、结构和接口

C#编译器提供一个partial关键字,以允许将一个类、结构或接口定义在多个文件里。

在编译时,编译器自动将类、结构或接口的各部分合并起来。这仅是C#编译器提供的一个功能,CLR对此一无所知。

常量

常量就是代表一恒定数据值的符号,比如我们将圆周率3.12415926定义成名为PI的常量,使代码更容易阅读。而且常量是在编译时就代入运算的(常量就是一个符号,编译时编译器就会将该符号替换成实际值),不会造成任何性能上的损失。但这一点也可能会造成一个版本问题,即假如未来修改了常量所代表的值,那么用到此常量的地方都要重新编译(我个人认为这也是常量名称的由来,我们应该将恒定不变的值定义为常量,以免后期改动时产生版本问题)。下面的示例也验证了这一点,Test1和Test2方法内部的常量运算在编译后,就已经运算完成。

从上面示例,我们还能看出一点:常量key和value编译后是静态成员,这是因为常量通常与类型关联而不是与实例关联,从逻辑上说,常量始终是静态成员。但对于在方法内部定义的常量,由于作用域的限制,不可能有方法之外的地方引用到这个常量,所以在编译后,常量被优化了。

字段

字段是一种数据成员,在OOP的设计中,字段通常是用来封装一个类型的内部状态,而方法表示的是对这些状态的一些操作。

在C#中字段可用的修饰符有

  • Static 声明的字段与类型关联,而不是与对象关联(默认情况下字段与对象关联)
  • Readonly 声明的字段只能在构造器里写入值(可以通过反射修改)
  • Volatile 声明的字段为易失字段(用于多线程环境)

这里要注意的是将一个字段标记为readonly时,不变的是引用,而不是引用的值。示例:

    class ReadonlyField
    {
        //chars 保存的是一个数组的引用
        public readonly char[] chars = new char[] { 'A', 'B', 'C' };

        void Main()
        {
            //以下改变数组内存,可以改成功
            chars[0] = 'X';
            chars[1] = 'Y';
            chars[2] = 'Z';

            //以下更改chars引用,无法通过编译
            chars = new char[] { 'X', 'Y', 'Z' };
        }
    }

属性

CLR支持两种属性:无参属性和有参属性(C#中称为索引器)。

面向对象设计和编程的重要原则之一就是数据封装,这意味着字段(封装对象的内部状态)永远不应该公开。因此,CLR提供属性机制来访问字段内容(VS中输入propfull加两次Tab会为我们自动生成字段和属性的代码片断)。

下面的示例中,Person对象内部有一个表示年龄的字段,如果直接公开这个字段,则不能保存外部不会将age设置为0或1000,这显然是没有意义的(也破坏了数据封装性),所以通过属性,可以在操作字段时,加一些额外逻辑,以保证数据的有效性。

 class Person
    {
        //Person对象的内部状态
        private int age;

        //用属性来安全地访问字段
        public int Age
        {
            get { return age; }
            set
            {
                if (value > 0 && value <= 150) age = value;
                else { }    //抛出异常
            }
        }
    }

编译上述代码后,实际上编译器会将属性内的get和set访问器各生成一个方法,方法名称是get_和set_加上属性名,所以说属性的本质是方法。

如果只是为了封装一个字段而创建一个属性,C#还为我们提供了一种更简单的语法,称为自动实现的属性(AIP)。下面是一个示例(在VS中输入prop加两次TAB会为我们生成AIP片断):

这里要注意一点,由于AIP的支持字段是编译器自动生成的,而且编译器每次编译都可能更改这个名称。所以在任何要序列化和反序列化的类型中,都不要使用AIP功能。

对象和集合初始化器

在实现编程中,我们经常构造一个对象,然后设置对象的一些公共属性或字段。为此C#为我们提供了一种简化的语法来完成这些操作。如下示例:

    class Person
    {
        //AIP
        public string Name { get; set; }
        public int Id { get; set; }
        public int Age { get; set; }

        void Main()
        {
            //没有使用对象初始化器的语法
            Person p1 = new Person();
            p1.Id = 1;
            p1.Name = "Heku";
            p1.Age = 24;

            //使用对象初始化器的语法
            Person p2 = new Person() { Id = 1, Name = "Heku", Age = 24 };
        }
  }

使用对象初始化器的语法时,实际上编译器为我们生成的代码和上面是一致的,但是下面的代码明显更加简洁。如果本来就是要调用类型的无参构造器,C#还允许我们省略大括号之前的小括号:

Person p2 = new Person { Id = 1, Name = "Heku", Age = 24 };

如果一个属性的类型实现了IEnumerable或IEnumerable<T>接口,那么这个属性就被认为是一个集合,我们同样类似的语法来初始化一个集合。比如我们在上例中的Person类中加入一个新属性Skills

public List<string> Skills { get; set; }

然后可以用下面的语法来初始化

//使用简化的对象初始化器语法+简化集合初始化器语法
Person p3 = new Person { Id = 1, Name = "heku", Age = 24, Skills = new List<string> { "C#", "jQuery" } };

这里我们用new List<string> { "C#", "jQuery" }一句来初始化了一个集合(实现上new List<string>完全可以省略,编译器会根据属性的类型来自动推断集合类型),并添加了两项纪录。编译器会我们生成的代码看起来是这样的:

p3.Skills = new List<string>();
p3.Skills.Add("C#");
p3.Skills.Add("jQuery");

有参属性

前面讲到的属性都没有参数,实现上还有一种可以带参数的属性,称之为有参属性(C#中叫索引器)。

    class StringArray
    {
        private string[] array;

        public StringArray()
        {
            array = new string[10];
        }

        //有参属性
        public string this[int index]
        {
            get
            {
                return array[index];
            }
            set
            {
                array[index] = value;
            }
        }

        void Main()
        {
            StringArray array = new StringArray();
            array[0] = "Hello";
            array[1] = "World";

            string ss = array[0] + array[1];
        }
    }

上面的例子中,和定义无参属性不同的是,这里并没有属性名称,而是用this[参数]的语法来定义一个有参属性(索引器),这是C#的要求。和无参属性不同,有参属性还支持重载:

        //有参属性
        public string this[int index]
        {
            get { return array[index]; }
            set { array[index] = value; }
        }

        //有参属性重载
        public string this[int index, bool isStartFromEnd]
        {
            get
            {
                if (isStartFromEnd) return array[10 - index];
                else return array[index];
            }
            set
            {
                if (isStartFromEnd) array[10 - index] = value;
                else array[index] = value;
            }
        }

属性本质是方法,有参属性也一样(对CLR来说甚至并不分有参还是无参,对它来说都是方法的调用),那么有参属性的编译后生成的IL是什么样子呢?事实上C#对所有的有参属性生成的IL方法都默认命名为get_Item和set_Item。当然这是可以通过在索引器上应用System.runtime.CompliserServices.IndexerNameAttribute定制Attribute来改变这一默认行为。

属性访问器的可访问性

属性的get和set访问器是可以定义不同的访问性的,如果get和set访问器的可访问性不一致,C#要求必须为属性本身指定限制最小的那一个。

 protected string Name
 {
     get { return name; }
     private set { name = value; }
 }

注意:如果同时设置get和set的访问性,会提示“不能为属性的两个访问器同时指定可访问性修改符”,因为对属性或索引器使用访问修饰符受以下条件的制约:

  • 不能对接口或显式接口成员实现使用访问器修饰符
  • 仅当属性或索引器同时具有 set 和 get 访问器时,才能使用访问器修饰符。这种情况下,只允许对其中一个访问器使用修饰符
  • 如果属性或索引器具有 override 修饰符,则访问器修饰符必须与重写的访问器的访问性(如果有的话)匹配
  • 访问器的可访问性级别必须比属性或索引器本身的可访问性级别具有更严格的限制
  • 属性没有存储数据的功能,数据都存在字段中,所以只有修改字段的数据才能更改数据,修改属性的值没用。
  • 原文地址
时间: 2024-12-10 09:54:20

C# 类型和成员基础以及常量、字段、属性的相关文章

[CLR via C#]6. 类型和成员基础

原文:[CLR via C#]6. 类型和成员基础 6.1 类型的各种成员 在一个类型中,可以定义0个或多个以下种类的成员: 1)常量    常量就是指出数据值恒定不变的符号.这些符号通常用于使代码更容易阅读和维护.常量通常与类型关联,而不与类型的实例关联.从逻辑上讲,常量始终是静态成员. 2)字段    字段表示一个只读或可读/写的数据值.字段可以是静态的,这时是类型状态的一部分:字段也可以是实例(非静态)的,这时字段是对象状态的一部分.强烈建议将字段声明成为私有字段,防止类型或对象状态被外部

CLR笔记:6.类型和成员基础

1.Class的可见性有public和internal两种,public对所有程序集都可见,internal仅对其所在的程序 集可见.默认是public的. 2.友元程序集, 使用friend assembly可以实现单元测试,而不使用反射技术. 书上讲的是按照命令行编译. 我测试用的是vs2005的solution,如下: 3.成员的可访问性 成员默认是private的,接口类型的成员都是public的. 子类重写父类的成员时,原始成员与重写成员要有相同的可访问性--C#的约束:CLR的约束是

Swift语言指南(一) Swift语言基础:常量和变量

Swift 是开发 iOS 及 OS X 应用的一门新编程语言,然而,它的开发体验与 C 或 Objective-C 有很多相似之处. Swift 提供了 C 与 Objective-C 中的所有基础类型,包括表示整数的 Int,表示浮点数的 Double 与 Float,表示布尔值的 Bool,以及表示纯文本数据的 String. Swift 还为两个基本集合类型 Array 与 Dictionary 提供了强大的支持,详情可参考 (集合类型)Collection Types. 与 C 语言类

Swift语言指南(一)--语言基础之常量和变量

原文:Swift语言指南(一)--语言基础之常量和变量 Swift 是开发 iOS 及 OS X 应用的一门新编程语言,然而,它的开发体验与 C 或 Objective-C 有很多相似之处. Swift 提供了 C 与 Objective-C 中的所有基础类型,包括表示整数的 Int,表示浮点数的 Double 与 Float,表示布尔值的 Bool,以及表示纯文本数据的 String. Swift 还为两个基本集合类型 Array 与 Dictionary 提供了强大的支持,详情可参考 (集合

java接口类型作为成员变量,请教下大家

问题描述 java接口类型作为成员变量,请教下大家 先有一个接口A,类B实现了接口A,而接口A的引用作为类B的一个成员变量,怎么理解这种写法??新手不太明白,谢谢大家 解决方案 面向借口的变成有一个最大的好处是方便扩展.B类中有一个成员变量的类型是A,比如说再有一个C impl A,那么B 中可以存放C对象,如果还有另外一个D也impl了A,同样也可以存放D的实例化对象(java的三大特性之一:多态),在不确定的情况下,就只好定义接口A,又下级调用者赋值,最根本的目的是为了程序的可扩展性. 解决

c++构造函数的初始化列表中初始化了一个类类型的成员,调用的是类类型的复制构造函数吧?

问题描述 c++构造函数的初始化列表中初始化了一个类类型的成员,调用的是类类型的复制构造函数吧? 如题:c++构造函数的初始化列表中初始化了一个类类型的成员,调用的是类类型的复制构造函数吧? 解决方案 什么?复制构造函数? 解决方案二: C++类构造函数列表初始化C++类构造函数初始化列表c++中什么类型的成员变量只能在构造函数的初始化列表中进行 解决方案三: 真是初始化列表,跟复制构造函数没关系,调用复制构造函数要看你是如何初始化的 解决方案四: 参数列表初始化成员是调用成员的构造函数,但是什

用C#编程遇到错误 说是产生相同参数类型的成员

问题描述 错误类型"xpButton.xpGlowButton"已定义了一个名为"InitializeComponent"的具有相同参数类型的成员,双击进入错误位置会提示在C#编程错误类型"xpButton.xpGlowButton"已定义了一个名为"InitializeComponent"的具有相同参数类型的成员:双击进入错误会提示在xpGlowButton.Designer.cs文件中仍有一处定义的InitializeCo

web-ASP.NET错误 6 类型已定义了一个名为“Page_Load”的具有相同参数类型的成员

问题描述 ASP.NET错误 6 类型已定义了一个名为"Page_Load"的具有相同参数类型的成员 错误 6 类型"Web_MetenLive.zhuanti.Chongqing.Inf02"已定义了一个名为"Page_Load"的具有相同参数类型的成员 E:项目MetenSiteMetenSiteMetenSiteMetenSite.Web(MetenLive)zhuantiChongqingInf02.aspx.cs 12 24 Meten

成员变量是指字段和属性吗?

问题描述 如题..成员变量是神马?等价与字段和属性吗? 解决方案 解决方案二:类,结构里面定义的字段,只是说法上的问题解决方案三:c++中一般称成员变量,C#中一般称字段,两者等价.但我认为成员变量不等价于属性,属性的内涵更丰富些,而且属性的实质是方法,而不是变量.解决方案四:方法.字段是从面向对象的角度来说的(语言无关)函数.属性是从语言的角度来说的.比如说C#有属性的概念,但是Java就没有.解决方案五:等价于字段解决方案六:字段的别名,一个意思解决方案七:字段.属性.方法.事件,统称成员.