zoukankan      html  css  js  c++  java
  • C#创建类型

    类(Class)

    最常见的一种引用类型

    class YourClassName
    {
    }

    class前面可以加上一些关键字,比如public、private、abstract、static、readonly

    class后面是这个类的名称,类型名称后面也可以跟一些东西,比如泛型类、继承的父类等

    字段(Fields)

    是Class或Struct的成员,它是一个变量

    class Octopus
    {
        string name;
        public int Age = 10;
    }

    readonly修饰符

    • readonly修饰符防止字段在构造之后被改变
    • readonly字段只能在声明的时候被赋值,或在构造函数里被赋值

    字段的初始化

    • 字段可以可选的初始化
    • 未初始化的字段有一个默认值
    • 字段的初始化在构造函数之前运行

    同时声明多个字段

    static readonly int legs = 8, eyes = 2;

    方法

    • 一个方法通常有一些语句组成,执行某个动作
    • 参数
    • 返回类型
    • void
    • ref/out

    方法的签名

    类型类方法的签名必须唯一

    签名:方法名、参数类型(含顺序,但与参数名称和返回类型无关)

    Expression-bodied methods (C# 6)

    int Foo (int x)
    {
    return x * 2;
    }
    int Foo (int x) => x * 2;
    void Foo (int x) => Console.WriteLine (x);

    第一个是一个常规的写法,第二个就是第一个的Expression-bodied方法的形式。当然没有返回类型的也可以这样写,这种写法只适用于一些简单的方法,单表达式的方法。

    方法的重载(Overloading methods)

    类型里的方法可以进行重载(允许多个同名的方法同时存在),只要这些方法的签名不同就行

    void Foo (int x) {...}
    void Foo (double x) {...}
    void Foo (int x, float y) {...}
    void Foo (float x, int y) {...}

    在看一个特殊点的方法的例子

    void Foo (int x) {...}
    float Foo (int x) {...} // 编译时报错,签名相同,因为签名不包含方法的返回类型
    void Goo (int[] x) {...}
    void Goo (params int[] x) {...} // 编译时错误,签名相同,因为签名不包含参数类型的修饰符(params去掉就是一样的)

    按值传递 和 按引用传递

    参数是按值传递的还是按引用传递的,也是方法签名的一部分

    void Foo (int x) {...}
    void Foo (ref int x) {...} // 是可以的

    因为参数的传递方式是不同的,一个是值类型,一个是引用类型,所以签名不一致,是可以的,下面这样就不行了:

    void Foo (int x) {...}
    void Foo (ref int x) {...} // 是可以的
    void Foo (out int x) {...} // 编译时错误

    后两个方法的参数类型和方法名都是一样的,而且都是按引用传递,所以后两个的签名是一样的

    本地方法(C# 7)

    一个方法在另一个方法的里面,就叫本地方法(Local methods)

    void WriteCubes()
    {
        Console.WriteLine (Cube (3));
        Console.WriteLine (Cube (4));
        Console.WriteLine (Cube (5));
        int Cube (int value) => value * value * value;
    }
    • 本地方法仅对封闭方法可见,Cube仅在WriteCubes方法内可见
    • 本地方法可以出现在其他函数类型中,例如属性访问器、构造函数等
    • 本地方法也可以是迭代器或异步方法
    • static修饰符对于本地方法是无效的

    构造函数

    • 运行class或struct的初始化代码
    • 和方法差不多,方法名和类型一致,返回类型也和类型一致,但不写了

     看一个构造的例子:

    public class Panda
    {
        string name; // 定义字段
        public Panda (string n) // 定义构造函数
        {
            name = n; // 初始化代码
        }
    }
    
    ……
    Panda p = new Panda ("Petey"); // 调用构造函数

    构造函数允许以下修饰符

    • 访问修饰符: public internal private protected
    • 非托管代码修饰符: unsafe extern

    从C#7开始,允许单语句的构造函数写成expression-bodied成员的形式,上面的Panda类的构造函数可以写成如下形式:

    public Panda (string n) => name = n;

    构造函数的重载

    • class和struct可以重载构造函数
    • 调用重载构造函数时使用this

    看下例子:

    public class Wine
    {
        public decimal Price;
        public int Year;
    
        public Wine (decimal price) 
        {
            Price = price;
        }
    
        public Wine (decimal price, int year) : this (price)
        {
            Year = year;
        }
    }
    • 当同一个类型下的构造函数A调用构造函数B的时候,B先执行

    看下下面的例子:

    public class Wine
    {
        public decimal Price;
        public int Year;
    
        public Wine (decimal price) 
        {
            Price = price;
            Console.WriteLine("1");
        }
    
        public Wine (decimal price, int year) : this (price)
        {
            Year = year;
            Console.WriteLine("2");
        }
    }

    调用构造函数var wine = new Wine(25m, 1988);

    结果是先输出1,再输出2

    • 可以把表达式传递给另一个构造函数,但表达式本身不能使用this引用,因为这时候对象还没有被初始化,所以任何方法的调用都会失败,但是可以使用static方法。

    无参构造函数

    • 对于class,如果你没有定义任何构造函数,那么C#编译器会自动生成一个无参的public构造函数
    • 但是如果你定义了构造函数,那么这个无参的构造函数就不会被编译器生成了

    构造函数和字段的初始化顺序

    • 字段的初始化发生在构造函数执行之前
    • 字段按照声明的先后顺序进行初始化

    非public的构造函数

    构造函数可以不是public的,看个例子:

    public class Class1
    {
        Class1() {} // 私有的构造函数
        
        public static Class1 Create (...)
        {
            // 在这里执行自定义的逻辑来返回Class1的实例
            ...
        }
    }

    那么获取类的实例则无法在通过new关键字获取了,而是通过类内部的创建实例的静态方法获取:var instance = Class1.Create();

    单例模式则可以这么写

    Deconstructors (C# 7)

    C#7引入了deconstructor模式,作用基本和构造函数相反,它会把字段反赋给一堆变量

    有一些要求,比如方法名必须是Deconstruct,有一个或多个out参数

    class Rectangle
    {
        public readonly float Width, Height;
        public Rectangle (float width, float height)
        {
            Width = width;
            Height = height;
        }
        public void Deconstruct (out float width, out float height)
        {
            width = Width;
            height = Height;
        }
    }

    调用析构函数,我们可以下面的方式

    var rect = new Rectangle (3, 4);
    (float width, float height) = rect; // Deconstruction
    Console.WriteLine (width + " " + height); // 3 4

    第二行就是调用析构函数,当然,它也可以这么写,似乎更好理解一点

    float width, height;
    rect.Deconstruct (out width, out height);

    或者以下的方法都是可以的:

    rect.Deconstruct (out var width, out var height);
    //或者这样
    (var width, var height) = rect;
    //或这样
    var (width, height) = rect;
    //或这样
    float width, height;
    (width, height) = rect;

    析构函数,可以重载,也可以是扩展方法,扩展方法这里就不展开说了

    对象初始化器(Object Initializers)

    对象任何可访问的字段/属性在构造之后,可通过对象初始化器直接为其进行设定值

    下面是一个Bunny类,包含一个无参的构造函数和一个有参的构造函数

    public class Bunny
    {
        public string Name;
        public bool LikesCarrots;
        public bool LikesHumans;
        public Bunny () {}
        public Bunny (string n) { Name = n; }
    }

    使用对象初始化器实例化Bunny对象

    // 无参的构造函数可以省略掉小括号
    Bunny b1 = new Bunny { Name="Bo", LikesCarrots=true, LikesHumans=false };
    Bunny b2 = new Bunny ("Bo") { LikesCarrots=true, LikesHumans=false };

    上面的这个对象初始化器的代码,就相当于下面的这段代码:

    Bunny temp1 = new Bunny(); // temp1 是编译器生成的名称
    temp1.Name = "Bo";
    temp1.LikesCarrots = true;
    temp1.LikesHumans = false;
    
    Bunny b1 = temp1;
    
    Bunny temp2 = new Bunny ("Bo");
    temp2.LikesCarrots = true;
    temp2.LikesHumans = false;
    
    Bunny b2 = temp2;

    临时变量是为了确保如果在初始化过程中发生了异常,那么不会以一个初始化了一半的对象而结束

    对象初始化器与可选参数

    如果不使用初始化器,上面例子中的构造函数也可以使用可选参数:

    public Bunny (string name, bool likesCarrots = false, bool likesHumans = false)
    {
        Name = name;
        LikesCarrots = likesCarrots;
        LikesHumans = likesHumans;
    }

    那么在调用的时候可以这么调用,name是必须的,后面两个参数可传可不传:

    Bunny b1 = new Bunny (name: "Bo", likesCarrots: true);

    可选参数方式有个优点,可以做到让Bunny类的字段/属性只读,也有个缺点,每个可选参数的值都被嵌入到了调用栈(calling site),C#会把构造函数的调用翻译成下面的形式:

    Bunny b1 = new Bunny ("Bo", true, false);

    其实我们使用了两个参数,最后一个值我们没有用,但是它把这个值嵌入进去(默认值)

    这个可选参数的缺点,有时候可能会引起一些问题,比如,我们在另一个程序集里实例化这个类,然后为这个Bunny再添加了一个参数,变成4个参数了,这个时候就可能有问题,因为如果另一个程序集没有重新编译,那么调用的就还是原来的三个参数的构造函数,然后一运行运行时就会报错了;还有一种情况是,如果改变了其中一个可选参数的值,那么其他程序集的调用者调用的还是会使用原来旧的参数值,直到那些程序集被重新编译。

    this 引用

    this引用指的是实例的本身

    public class Panda
    {
        public Panda Mate;
        public void Marry (Panda partner)
        {
            Mate = partner;
            partner.Mate = this;
        }
    }

    this引用可以让你把字段与本地变量或参数区分开

    public class Test
    {
        string name;
        public Test (string name)
        {
            this.name = name;
        }
    }

    只有class/struct的非静态成员才可以使用this

    属性(Properties)

    从外部来看,属性和字段很像,但从内部看,属性含有逻辑,就像方法一样

    Stock msft = new Stock();
    msft.CurrentPrice = 30;
    msft.CurrentPrice -= 3;
    Console.WriteLine (msft.CurrentPrice);

    我们从外面看不出来CurrentPrice是属性还是字段

    属性的声明

    属性的声明和字段的声明很像,但是多了一个get、set块

    public class Stock
    {
        decimal currentPrice; // The private "backing" field
    
        public decimal CurrentPrice // The public property
        {
            get { return currentPrice; }
            set { currentPrice = value; }
        }
    }

    属性的get set

    • get/set代表属性的访问器
    • get访问器会在属性被读取的时候运行,必须返回一个该属性类型的值
    • set访问器会在属性被赋值的时候运行,有一个隐式的该类型的参数value,通常你会把value赋值给一个私有字段

    属性与字段的区别

    尽管属性的访问方式与字段的访问方式相同,但不同之处在于,属性赋予了实现者对获取和赋值的完全控制权。这种控制允许实现者选择任意所需的内部表示,不向属性的使用者公开其内部实现细节。

    只读和计算的属性

    如果属性只有get访问器,那么它就是只读的

    如果属性只有set访问器,那么它就是只写的(很少这样用)

    属性通常拥有一个专用的“幕后”字段(backing field),这个幕后的字段用来存储数据

    decimal currentPrice, sharesOwned;
    public decimal Worth
    {
        get { return currentPrice * sharesOwned; }
    }

    Expression-bodied 属性

    从C#6开始,你可以使用Expression-bodied形式来表示只读属性

    public decimal Worth => currentPrice * sharesOwned;

    C#7,允许set访问器也可以使用这种形式

    public decimal Worth
    {
        get => currentPrice * sharesOwned;
        set => sharesOwned = value / currentPrice;
    }

    自动属性

    属性最常见的一种实践是:setter和getter只是对private field进行简单的直接的读写。自动属性声明就告诉编译器来提供这种实现

    public class Stock
    {
        ...
        public decimal CurrentPrice { get; set; }
    }

    编译器会自动生成一个幕后的私有字段,其名称不可引用

    set访问器也可以是private或protected

    属性初始化器

    从C#6开始,你可以为自动属性添加属性初始化器

    public decimal CurrentPrice { get; set; } = 123;

    只读的自动属性也可以使用(只读自动属性也可以在构造函数里被赋值)

    public int Maximum { get; } = 999;

    get 和 set 的访问性

    get和set访问器可以拥有不同的访问级别,典型的用法:public get,internal/private set

    public class Foo
    {
        private decimal x;
        public decimal X
        {
            get { return x; }
            private set { x = Math.Round (value, 2); }
        }
    }

    注意,通常属性的访问级别更宽松一些,访问器的访问级别更严一些(上面例子中的属性X的public和set的private)

    CLR的属性实现

    C#的属性访问器内部会编译成get_XXX 和 set_XXX

    public decimal get_CurrentPrice {...}
    public void set_CurrentPrice (decimal value) {...}

    简单的非virtual属性访问器会被JIT编译器进行内联(inline)操作,这会消除访问属性与访问字段之间的性能差异。内联是一种优化技术,它会把方法调用换成直接使用方法体。

    索引器

    索引器提供了一种可以访问封装了列表值或字典值的class/struct的元素的一种自然的语法

    string s = "hello";
    Console.WriteLine (s[0]); // 'h'
    Console.WriteLine (s[3]); // 'l'

    语法很像使用数组时用的语法,但是这里的索引参数是可以是任何类型的

    索引器和属性拥有同样的修饰符

    可以按照下列方式使用null条件操作符:

    string s = null;
    Console.WriteLine (s?[0]);

    实现索引器

    需要定义一个this属性,并通过中括号指定参数

    class Sentence
    {
        string[] words = "The quick brown fox".Split();
        public string this [int wordNum] // 索引器
        {
            get { return words [wordNum]; }
            set { words [wordNum] = value; }
        }
    }

    使用索引器

    Sentence s = new Sentence();
    Console.WriteLine (s[3]); // fox
    s[3] = "kangaroo";
    Console.WriteLine (s[3]); // kangaroo

    多个索引器

    一个类型可以声明多个索引器,它们的参数类型可以不同

    一个索引器可以有多个参数

    public string this [int arg1, string arg2]
    {
        get { ... } set { ... }
    }

    只读索引器

    如果不写set访问器,那么这个索引器就是只读的

    在C#6以后,也可以使用Expression-bodied语法

    public string this [int wordNum] => words [wordNum];

    CLR的索引器实现

    索引器在内部会编译成get_Item和set_Item方法

    public string get_Item (int wordNum) {...}
    public void set_Item (int wordNum, string value) {...}

    常量

    • 一个值不可以改变的静态字段,在编译时值就已经定下来了
    • 任何使用常量的地方,编译器都会把这个常量替换为它的值
    • 常量的类型可以是内置的数值类型、bool、char、string或enum
    • 使用const关键字声明,声明的同时必须使用具体的值来对其初始化

    一个常量的例子:

    public class Test
    {
        public const string Message = "Hello World";
    }

    常量与静态只读字段

    常量比静态只读字段更严格:

    体现在两个方面,一个是可使用的类型,第二个是字段初始化的语义上

    常量是在编译时进行值得估算的

    public static double Circumference (double radius)
    {
        return 2 * System.Math.PI * radius;
    }

    编译成下面的形式:

    public static double Circumference (double radius)
    {
        return 6.2831853071795862 * radius;
    }

    有一点需要注意,当值有可能改变,并且需要暴露给其它程序集的时候,静态只读字段是相对较好的选择

    public const decimal ProgramVersion = 2.3;

    如果Y 程序集引用了X程序集并且使用了这个常量,那么在编译的时候,2.3这个值就会固化在Y程序集里,这意味着,如果后来X重新编译了,这个常量变成了2.4,如果Y不重新编译的话,Y仍将使用2.3这个值,直到Y被重新编译,它的值才会变成2.4.静态只读字段就会避免这个问题的发生。

    常量不仅仅可以作为类型的字段,它还可以作为本地常量,在方法里声明常量,例如:

    static void Main()
    {
        const double twoPI = 2 * System.Math.PI;
        ...
    }

    静态构造函数

    • 静态构造函数,每个类型执行一次
    • 非静态构造函数,每个实例执行一次
    • 一个类型只能定义一个静态构造函数,并且必须无参、方法名与类型一致
    class Test
    {
        static Test()
        {
            Console.WriteLine ("Type Initialized");
        }
    }
    • 在类型使用之前的一瞬间,编译器会自动调用类型的静态构造函数:

    一个是实例化一个类型之前,一个是访问类型的一个静态成员之前

    • 只允许使用unsafe 和 extern 修饰符

    如果静态构造函数跑出来未处理的异常,那么这个类型在该程序的剩余生命周期内将无法使用了

    静态构造函数和字段初始化顺序

    • 静态字段的初始化器在静态构造函数被调用之前的一瞬间运行
    • 如果类型没有静态构造函数,那么静态字段初始化器在类型被使用之前的一瞬间执行,或者更早,在运行时突发奇想的时候就执行了
    • 静态字段的初始化顺序与它们的声明顺序一致
    class Foo
    {
        public static int X = Y; // 0
        public static int Y = 3; // 3
    }

    因为是按照声明的顺序初始化的,所以X的时候Y还没有,所以是0。

    class Program
    {
        static void Main() { Console.WriteLine (Foo.X); } // 3
    }
    class Foo
    {
        public static Foo Instance = new Foo();
        public static int X = 3;
        Foo() { Console.WriteLine (X); } // 0
    }

    在类Program里,输出的是3,因为在使用Foo类之前这个类型就已经初始化完成,所以是3,而在Foo类里面,则按照什么的先后顺序分别初始化静态构造函数,然后是静态字段,所以静态构造函数输出的是0,X还没有初始化。

    静态类

    类也可以是静态的,但是有个要求,就是其成员也必须全是静态的,不可以有子类。

    例如:System.Console 、System.Math 等

    终结器(Finalizers)

    Finalizer是class专有的一种方法,在GC回收未引用对象的内存之前运行,其实就是对object的Finalize()方法重写的一种语法

    这个很少用,例子:

    class Class1
    {
        ~Class1()
        {
            ...
        }
    }

    编译生成的是这样的

    protected override void Finalize()
    {
        ...
        base.Finalize();
    }

    C#7可以这样写:

    ~Class1() => Console.WriteLine ("Finalizing");
  • 相关阅读:
    Java蛇形数组的简单实现代码
    Android Studio生成javadoc出错的解决办法
    AngularJS指令嵌套时link函数执行顺序的问题
    [转]如果我有jQuery背景,我应该如何切换到AngularJS的思维模式?
    扩展ViewFlow避免和ViewPager滑动冲突,同时支持无限循环,并完美和CircleFlowIndicator结合
    人机交互的新方向:智能聊天机器人
    利用python自动清除Android工程中的多余资源
    巧用svn create patch(打补丁)方案解决定制版需求
    【Android开发坑系列】之经常被忽略的背景图片内存泄露
    【Android开发坑系列】之try-catch
  • 原文地址:https://www.cnblogs.com/njabsky/p/13460245.html
Copyright © 2011-2022 走看看