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

简介: 类型和成员基础 在C#中,一个类型内部可以定义多种成员:常量、字段、实例构造器、类型构造器(静态构造器)、方法、操作符重载、转换操作符、属性、事件、类型。 类型的可见性有public和internal(默认)两种,前者定义的类型对所有程序集中的所有类型都可见,后者定义的类型只对同一程序集内部的所有类型可见: public class PublicClass {

类型和成员基础

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

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

 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 修饰符,则访问器修饰符必须与重写的访问器的访问性(如果有的话)匹配
  • 访问器的可访问性级别必须比属性或索引器本身的可访问性级别具有更严格的限制
  • 属性没有存储数据的功能,数据都存在字段中,所以只有修改字段的数据才能更改数据,修改属性的值没用。
  • 原文地址
相关文章
|
25天前
|
存储 安全 数据库连接
C#深度揭秘:常量的魅力和实践,一文让你从新手到专家
C#深度揭秘:常量的魅力和实践,一文让你从新手到专家
16 0
|
3月前
|
存储 安全 编译器
C# 11.0中的泛型属性:类型安全的新篇章
【1月更文挑战第23天】C# 11.0引入了泛型属性的概念,这一新特性为开发者提供了更高级别的类型安全性和灵活性。本文将详细探讨C# 11.0中泛型属性的工作原理、使用场景以及它们对现有编程模式的改进。通过深入了解泛型属性,开发者将能够编写更加健壮、可维护的代码,并充分利用C#语言的最新发展。
|
3月前
|
C# 开发者
C# 10.0引入常量插值字符串:编译时确定性的新篇章
【1月更文挑战第22天】在C# 10.0中,微软为开发者带来了一项引人注目的新特性——常量插值字符串。这一功能允许在编译时处理和计算字符串插值表达式,从而得到可以在编译时确定的常量字符串。本文将深入探讨C# 10.0中常量插值字符串的概念、工作原理、使用场景及其对现有字符串处理方式的改进,旨在帮助读者更好地理解和应用这一强大的新特性。
|
7月前
|
编译器 C#
C#之十七 局部类型
C#之十七 局部类型
17 0
|
1月前
|
Java C#
C#学习相关系列之多线程(七)---Task的相关属性用法
C#学习相关系列之多线程(七)---Task的相关属性用法
|
1月前
|
存储 C# 开发者
C#变量类型
C#变量类型
18 0
|
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中本地函数属性的用法、优势以及可能的应用场景,帮助读者更好地理解并应用这一新功能。
|
3月前
|
开发框架 .NET 编译器
C# 9.0中的目标类型新表达式:类型推断的又一进步
【1月更文挑战第16天】C# 9.0引入了目标类型新表达式,这是类型推断功能的一个重要扩展。通过目标类型新表达式,开发者在创建对象时可以省略类型名称,编译器会根据上下文自动推断所需类型。这一特性不仅简化了代码编写,还提高了代码的可读性和维护性。本文将详细介绍目标类型新表达式的语法、使用场景及其对C#编程的影响。
|
3月前
|
存储 C# 容器
掌握 C# 变量:在代码中声明、初始化和使用不同类型的综合指南
变量是用于存储数据值的容器。 在 C# 中,有不同类型的变量(用不同的关键字定义),例如: int - 存储整数(没有小数点的整数),如 123 或 -123 double - 存储浮点数,有小数点,如 19.99 或 -19.99 char - 存储单个字符,如 'a' 或 'B'。Char 值用单引号括起来 string - 存储文本,如 "Hello World"。String 值用双引号括起来 bool - 存储具有两个状态的值:true 或 false
37 2