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修饰的参数,相当于引用,在函数内部改变了该参数的值,在函数外部的实参也会被改变,如下所示:
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上车买票。
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值以及修改这个值。
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的情况。
这两个例子都如下所示:
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;}}
}
最后,还有自动属性,自动属性外部不能改,如果类中有一个特征是只希望外部能得到不能改(能改不能得),又没什么特殊处理,那么可以直接使用自动属性。
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内的方法。如下所示
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类型;装载数组。
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;//stringobject 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数组,那么可以传入任意类型:
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)拆箱
拆箱:把引用类型存储的值类型取出来,堆内存会迁移到栈内存中。
static void Main(string[] args){//装箱object v = 3;//拆箱int intValue = (int)v;}
四、抽象类和抽象方法
1、抽象类
被抽象关键字abstract修饰的类,特点是:不能被实例化、可以包含抽象方法、继承抽象类必须重写其抽象方法。
如下所示,我们定义了一个抽象类Thing,类Water继承了抽象类Thing。Thing是不能被实例化,也就是不能new的,但是Water可以。
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()是虚函数,可以自行选择是否重写。
abstract class Fruits
{public string name;//子类需要重写,不能是私有的public abstract void Bad();public virtual void Test(){//可以选择是否写逻辑}
}
子类Apple继承了父类Fruits,那么Apple必须重写抽象方法,否则就会报错。使用public override void Bad()。但是对于虚方法Test(),可以自行选择是否实现。
class Apple : Fruits
{//实现抽象方法public override void Bad(){}//可以选择是否实现public override void Test(){base.Test();}
}
如果还有类继承了Apple,抽象方法和虚方法都可以选择继续重写下去:
class SuperApple : Apple
{//可以选择继续重写下去public override void Bad(){base.Bad();}public override void Test(){base.Test();}
}
五、接口
1、概念
接口是行为的抽象规范,它也是一种自定义类型。关键字是interface。
接口声明的规范是:不能包含成员变量,只包含方法、属性、索引器、事件;成员不能被实现,成员可以不写访问修饰符,不能是私有的;接口不能继承类,但是可以继承另一个接口。
接口的使用规范是:类可以继承多个接口,类继承接口后,必须实现接口中所有成员。
2、实现
interface IFly { } 即可定义一个接口,接口的命名规范是帕斯卡命名法并在前面加个I。成员方法、属性等都不能实现,如下所示:
interface IFly {public void Fly();//protected也可以string Name{get; //也不能有语句块,属性也不能被实现set;}}
现有一类Person同时继承了类型Animal和接口IFly,它必须实现接口中的所有内容。而接口继承接口时,不需要实现,待类继承接口后,类自己去实现所有内容。
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(),如下例所示:
interface IAtk{void Atk();}interface ISuperAtk{void Atk();}class Player : IAtk, ISuperAtk{/*public void Atk(){}*///显式实现void IAtk.Atk(){}void ISuperAtk.Atk(){}public void Atk(){}}
