写在前面:
写本系列**(自用)** 的目的是回顾已经学过的知识、记录新学习的知识或是记录心得理解,方便自己以后快速复习,减少遗忘**。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()
{
}
}