C#知识补充(一)——ref和out、成员属性、万物之父和装箱拆箱、抽象类和抽象方法、接口

写在前面:

写本系列**(自用)** 的目的是回顾已经学过的知识、记录新学习的知识或是记录心得理解,方便自己以后快速复习,减少遗忘**。C#中某些内容在初学的时候没学明白,在这里巩固一下。所以知识点不会连贯,是零散的。**

一、ref和out

ref和out是函数参数的修饰符。

1、ref和out的使用

ref和out在使用上是相同的,需要在函数中传入ref或者out修饰的参数,只需要直接在函数参数前加上ref/out即可,像这样:

static void ChangeValueRef(ref int value)

同样的,在调用该函数时,传参也需要加上ref:

ChangeValueRef(ref a);

ref和out修饰的参数,相当于引用,在函数内部改变了该参数的值,在函数外部的实参也会被改变,如下所示:

cs 复制代码
class Program
{
    static void ChangeValueRef(ref int value)
    {
        value = 3;
    }

    static void ChangeValue(int value)
    {
        value = 3;
    }

    static void ChangeValueOut(out int value)
    {
        value = 5;
    }

    static void Main(string[] args)
    {
        int a = 1;

        ChangeValue(a);
        Console.WriteLine(a);

        ChangeValueRef(ref a);
        Console.WriteLine(a);

        ChangeValueOut(out a);
        Console.WriteLine(a);
    }
}

2、ref和out的区别

ref传入的变量必须初始化,out不用;out必须在内部赋值,ref不用。并且out无论在外部有没有初始化都视为没有初始化。

如下例所示,声明变量b但是不初始化b,如果将b传入ChangeValueRef()就会报错,但是可以传入函数ChangeValueOut()。

如果将ChangeValueOut()内部的value = 5注释了,也就是不在函数体内对value复制,那么ChangeValueOut()就会报错,ChangeValueRef()不会。

通俗来讲就是ref买票上车,out上车买票。

cs 复制代码
class Program
{
    static void ChangeValueRef(ref int value)
    {
        value = 3;
    }

    static void ChangeValueOut(out int value)
    {
        value = 5;
    }

    static void Main(string[] args)
    {
        int b;
        //ChangeValueRef(ref b);
        ChangeValueOut(out b);
        Console.WriteLine(b);
    }
}

二、成员属性

属性可以解决public、private、protected的局限性。属性可以让成员变量在外部只能获取,不能修改;或者只能修改,不能获取。

1、基本语法

属性的基本语法是:

访问修饰符 类型 属性名

{

get语句块

set语句块

}

其中,get必须有返回值,通过这个属性可以得到私有的成员变量。set语句块中有默认的value关键字,用于表示外部传入的值,value的类型和属性的类型一致。如下所示,定义了一个属性Name,并且在Main函数中通过p.Name获取到了私有的Name值以及修改这个值。

cs 复制代码
class Person 
{
    private string name;
    private int age;
    private int money;
    private bool sex;

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

internal class Program
{
    static void Main(string[] args)
    {
        Person p = new Person();
        //使用属性
        p.Name = "Max";
        Console.WriteLine(p.Name);
    }
}

2、用处

可以利用属性实现只能得不能改的效果,这种使用方式在单例模式中十分常见。get和set不能同时使用访问修饰符。

get和set可以只有一个。只有一个时,没必要加访问修饰符,一般是只有一个get, 即只让外部得到但不能改,基本不会出现只有一个set的情况。

这两个例子都如下所示:

cs 复制代码
class Person 
{
    private string name;
    private int age;
    private int money;
    private bool sex;

    public int Money
    {
        get
        {
            return money - 5;
        }
        private set
        {
            money = value + 5;
        }
    }

    public bool Sex
    {
        get
        {
            return sex;
        }
    }
}

最后,还有自动属性,自动属性外部不能改,如果类中有一个特征是只希望外部能得到不能改(能改不能得),又没什么特殊处理,那么可以直接使用自动属性。

cs 复制代码
class Person 
{
    private string name;
    private int age;
    private int money;
    private bool sex;

    public float Height
    {
        get;
        private set;
    }
}

三、万物之父和装箱拆箱

1、万物之父

万物之父是object,是所有类的基类,是一种引用类型。可以用里氏替换原则, 用object容器装所有的对象。object可以用来表示不确定的类型,作为函数参数类型。

(1)里氏替换原则

父类Father可以直接装载子类Son,并将Father "as" 为Son从而调用Son内的方法。如下所示

cs 复制代码
class Father
{

}

class Son : Father
{
    public void Speak()
    {

    }
}

internal class Program2
{
    
    static void Main(string[] args)
    {
        Father f = new Son();

        if(f is Son)
        {
            (f as Son).Speak();
        }

    }
}

(2)object

object就是所有类型的父对象,可以利用里氏替换原则用object容器装所有的对象。如下,可以利用object装载引用类型Son;装载值类型如整数、浮点数;装载string类型;装载数组。

cs 复制代码
internal class Program2
{
    
    static void Main(string[] args)
    {
        //引用类型
        object o = new Son();
        //o = f;
        if(o is Son)
        {
            (o as Son).Speak();
        }

        //值类型
        object o2 = 1f;
        float fl = (float)o2;

        //string
        object str = "12334";
        string str2 = str.ToString();
        //string str2 = str as string;

        //数组
        object arr = new int[10];
        int[] ar = arr as int[];

        //装箱
        object v = 3;
        //拆箱
        int intValue = (int)v;
    }
}

使用object声明的数组可以装载任意的类型,如下,函数的参数为object数组,那么可以传入任意类型:

cs 复制代码
internal class Program2
{
    
    static void Main(string[] args)
    {
        TestFun(1, 2, 3, 4f, "1234", new Son());
    }

    static void TestFun(params object[] array)
    {

    }
}

2、装箱拆箱

发生条件:用object存值类型(装箱),再把object转为值类型(拆箱)。使用object的好处是,在不确定类型时可以方便参数的存储和传递。坏处是,装箱拆箱存在内存迁移,增加性能消耗。

(1)装箱

装箱:把值类型用引用类型存储,栈内存会迁移到堆内存中。

(2)拆箱

拆箱:把引用类型存储的值类型取出来,堆内存会迁移到栈内存中。

cs 复制代码
    static void Main(string[] args)
    {
        //装箱
        object v = 3;
        //拆箱
        int intValue = (int)v;
    }

四、抽象类和抽象方法

1、抽象类

被抽象关键字abstract修饰的类,特点是:不能被实例化、可以包含抽象方法、继承抽象类必须重写其抽象方法。

如下所示,我们定义了一个抽象类Thing,类Water继承了抽象类Thing。Thing是不能被实例化,也就是不能new的,但是Water可以。

cs 复制代码
abstract class Thing
{
    public string name;
}

class Water :Thing
{

}

internal class Program3
{
    static void Main(string[] args)
    {
        //Thing t = new Thing(); 抽象类不能被实例化

        Thing t = new Water(); //可以遵循里氏替换原则
    }
}

2、抽象函数

抽象函数又叫纯虚方法,用abstract关键字修饰。只能在抽象类里声明,没有方法体。不能是私有的,因为抽象函数继承后必须实现,用override来重写。

如下所示,有抽象类Fruits。在Fruits中有抽象函数public abstract void Bad();,由于Bad()子类必须重写,所以不能是私有的,只能是公共or保护的。Fruits中public virtual void Test()是虚函数,可以自行选择是否重写。

cs 复制代码
abstract class Fruits
{
    public string name;

    //子类需要重写,不能是私有的
    public abstract void Bad();

    public virtual void Test()
    {
        //可以选择是否写逻辑
    }
}

子类Apple继承了父类Fruits,那么Apple必须重写抽象方法,否则就会报错。使用public override void Bad()。但是对于虚方法Test(),可以自行选择是否实现。

cs 复制代码
class Apple : Fruits
{
    //实现抽象方法
    public override void Bad()
    {
        
    }

    //可以选择是否实现
    public override void Test()
    {
        base.Test();
    }
}

如果还有类继承了Apple,抽象方法和虚方法都可以选择继续重写下去:

cs 复制代码
class SuperApple : Apple
{
    //可以选择继续重写下去
    public override void Bad()
    {
        base.Bad();
    }

    public override void Test()
    {
        base.Test();
    }
}

五、接口

1、概念

接口是行为的抽象规范,它也是一种自定义类型。关键字是interface。

接口声明的规范是:不能包含成员变量,只包含方法、属性、索引器、事件;成员不能被实现,成员可以不写访问修饰符,不能是私有的;接口不能继承类,但是可以继承另一个接口。

接口的使用规范是:类可以继承多个接口,类继承接口后,必须实现接口中所有成员。

2、实现

interface IFly { } 即可定义一个接口,接口的命名规范是帕斯卡命名法并在前面加个I。成员方法、属性等都不能实现,如下所示:

cs 复制代码
    interface IFly 
    {
        public void Fly();//protected也可以

        string Name
        {
            get; //也不能有语句块,属性也不能被实现
            set;
        }
    }

现有一类Person同时继承了类型Animal和接口IFly,它必须实现接口中的所有内容。而接口继承接口时,不需要实现,待类继承接口后,类自己去实现所有内容。

cs 复制代码
    class Animal
    {

    }

    class Person : Animal, IFly
    {
        public string Name 
        {
            get
            {
                return "1";
            }
            set
            {
            }
        }        

        public virtual void Fly()
        {
            
        }
    }

    static void Main(string[] args)
    {
        IFly f = new Person();//能父类装子类,不能new自己
    }

3、显式实现

如果一个类继承了多个接口,并且接口中有同名函数时,还是按之前的实现的话,就会让两个接口只有一种行为表现。

因此需要采用显式实现:void IAtk.Atk()和void ISuperAtk.Atk(),使用显式实现时,继承接口的类也可以继续有同名函数public void Atk(),如下例所示:

cs 复制代码
    interface IAtk
    {
        void Atk();
    }

    interface ISuperAtk
    {
        void Atk();
    }

    class Player : IAtk, ISuperAtk
    {
        /*
        public void Atk()
        {
            
        }
        */

        //显式实现
        void IAtk.Atk()
        {
            
        }

        void ISuperAtk.Atk()
        {

        }

        public void Atk()
        {

        }
    }
相关推荐
报错小能手2 小时前
C++笔记——STL list
c++·笔记
T.Ree.2 小时前
cpp_list
开发语言·数据结构·c++·list
laocooon5238578862 小时前
C++ 图片加背景音乐的处理
开发语言·c++
爱编程的鱼2 小时前
C# var 关键字详解:从入门到精通
开发语言·c#·solr
MATLAB代码顾问2 小时前
MATLAB实现TCN神经网络数值预测
开发语言·matlab
2301_796512522 小时前
Rust编程学习 - 如何利用代数类型系统做错误处理的另外一大好处是可组合性(composability)
java·学习·rust
南汐汐月3 小时前
重生归来,我要成功 Python 高手--day33 决策树
开发语言·python·决策树
koo3643 小时前
李宏毅机器学习笔记43
人工智能·笔记·机器学习
星释3 小时前
Rust 练习册 :Proverb与字符串处理
开发语言·后端·rust