【游戏开发笔记】编程篇_C#面向对象 {下}
@
7.定义类
7.1 C#中的类定义
(关键词Class)默认情况下是只能当前项目可以访问
(internal) class MyClass()
{
// Class mumber.
}
类的继承:
class ClassSon : MyClass
{
// class mumbers.
}
派生类的可访问性需低于基类
可以给一个类添加一个基类多个接口,(可以同时实现基类和接口的添加)但要注意,需先添加基类,再添加接口,之间用逗号连接。
public class MyClass : MyBase , IMyInterface
{
// Class mumbers.
}
7.1.1 接口的定义
(interface 关键字)
interface IMyInterface
{
// Interface mumbers.
}
修饰符用法一致,但是不能使用关键字 abstract 和 sealed(无意义)
7.1.2 修饰符
internal //在本项目中公开(默认)
public //完全公开
abstract或 internal abstract //类只能在当前项目中访问,不能实例化,只能被继承
public abstract //类可以在任意位置访问,不能实例化,只能被继承
sealed 或 internal sealed //类只能在当前项目中访问,不能被继承,只能实例化
public sealed // 类可以是任意位置被访问,不能被继承,只能实例化
7.2 System.Object
.NET Framework 必须支持的基本方法:
Object()
~Object()
Equals(object)
Equals(object, object)
RefereceEquals(object, object)
Tostring()
MemberwiseClone()
GetType()
GetHashCode()
7.3 构造函数和析构函数
构造函数
class Myclass()
{
public MyClass()
{
// Constructor code
}
}
构析函数
class MyClass
{
~MyClass()
{
// Destructor body.
}
}
当进行垃圾回收时,就执行析构函数中的代码,释放资源
一般情况下,实例化时,先执行System.Object.Object()
,之后执行这个类的基类构造函数,最后才执行该类的构造函数。
可以通过关键字base指定基类构造函数,如:
public class MyBaseClass
{
public MyBaseClass()
{
}
public MyBaseClass(int i)
{
}
}
public class MyDerivedClass : MyBaseClass
{
// base 后面可以是常量,也可以是变量
public MyDerivedClass() : base(5)
{
}
public MyDerivedClass(int i)
{
}
public MyDerivedClass(int i, int j)
{
}
}
若调用默认构造函数,则执行顺序为:
- 执行System.Object.Object()构造函数。
- 执行MyBaseClass.MyBaseClass(int i)构造函数。
- 执行MyDerivedClass.MyDerivedClass()构造函数。
也可以使用关键字this改变该类构造函数的执行顺序:
public class MyDerivedClass : MyBaseClass
{
public MyDerivedClass() : this(5, 6)
{
}
……
public MyDerivedClass(int i, int j) : base(i)
{
}
}
如果使用MyDerivedClass()构造函数,执行顺序如下:
- 执行System.Object.Object()构造函数。
- 执行MyBaseClass.MyBaseClass(int i)构造函数。
- 执行MyDerivedClass.MyDerivedClass(int i, int j)构造函数。
- 执行MyDerivedClass.MyDerivedClass()构造函数。
即使用base的构造函数调用时,首先会执行另一个构造函数,使用注意不要创建无限循环
7.4 结构类型
(关键字struct)
定义与使用方法和类相似
区别:
类是引用,结构是值类型。即在类之间的变量赋值是指向同一位置,一个变量发生改变,其他的随之改变,而结构的变量则不会牵一发而动全身
7.5 浅度和深度复制
浅度赋值:复制引用
深度复制:复制全部信息
8.定义类成员
public 成员可以由任意代码访问
private 成员只能由类中代码访问(默认)
internal 成员只能由项目内部代码访问
protected 成员只能由家族(类或派生类)中的代码访问
static 静态成员,只能通过定义他们的类来访问
8.1 定义类成员
- 字段
class MyClass
{
// .NET Framework中公共字段以PascalCasing方式命名
public int MyInt;
}
也可通过关键字readonly修饰,表示字段只能在初始化的时候赋值(执行构造函数时)
class MyClass
{
public readonly int MyInt = 13;
}
const 成员也是静态的不需要static修饰
- 方法
class MyClass
{
public string GetString( ) => return "here is a GaoYao."
}
virtual 方法可以重写
abstract 方法必须在非抽象类中重写(只能用于抽象类)
override 方法重写了一个基类方法(如果方法被重写,就必须使用该关键字)
extern 方法定义放在其他地方
如果使用了override,也可以使用sealed进行再修饰,即这个方法不能由派生类重写
public class MyDerivedClass : MyClass
{
public override sealed void DoSomething()
{
// Dervied class implementation, overrides base implementation.
}
}
- 属性
// 注意属性的命名方式
public int MyIntProp
{
get
{
// Property get code.
}
set
{
// Property set code.
}
}
get块必须有一个属性返回值,关键字value表示用户提供的属性值(不必考虑类型转换)
public int MyIntProp
{
get {return myInt; }
set
{
if( value >= 0 && value <= 10)
myInt = value;
}
}
只有赋值在0~10之间才会修改myInt,如果使用了无效值,则有四种选择:
什么也不做(如上)
给字段赋默认值
继续执行,就像没有错误发生一样,但记录下该事件,已备将来分析
抛出异常
(一般推荐使用有两种情况)
属性也可使用关键字:virtual、override和abstract
,但不能用于字段
-
重构成员
表示使用工具修改代码
(右击类图中的某个成员)作用:自动添加字段的属性
-
自动属性
输入prop后按Tap键两次,就自动创建:
public int MyProperty {get; set;}
只有get存储的自动属性,和自动属性的初始化(无只写的自动属性)
public int MyIntProp {get; }
public int MyIntProp {get; } = 9;
8.2 类成员的其他主题
8.2.1 隐藏和重写基类方法
隐藏:派生类共享基类方法,因此调用时分不清楚。将基类方法隐藏,再在派生类中写一个“一模一样”的方法。
public class MyBaseClass
{
public void Dosomething()
{
// Base implemetation.
}
}
public class MyDeviedClass : MyBaseClass
{
// 使用关键字new显示说明
new public void Dosomething()
{
// Derived class implemetation, hides base implemetation.
}
}
(不使用new编译器会有一个警告)
重写后基类失去功能
public calss MyBaseClass
{
public virtual void DoSomething() => WriteLine('Base imp");
}
public class MyDervivedClass : MyBaseClass
{
public orrived void Dosomething() => WriteLine('Derived imp");
}
8.2.2 this和base
base和this可在类中使用
base:在派生类中,表示基类的方法
如之前的例子,则base.DoSomething()
表示基类的方法(即使将基类方法隐藏,也可在派生类内部访问)
this:1、把当前实例传递给一个方法。
public void DoSomething()
{
MyTargetClass myObj = new MyTargetClass;
myObj.DoSomethingWith(this);
}
2、限定局部类型成员
public class MyClass
{
private int someData;
public int someData
{
get
{
return this.someData;
}
}
}
显而易见,someData是引用成员,而不是变量。
8.2.3 嵌套的类型定义
public class MyClass
{
public class MyNestedClass
{
public int NestedClassField;
}
}
在MyClass外部实例化MyNestedClass,必须限定名称
MyClass.MyNestedClass myObj = new MyClass.MyNestedClass();
该功能主要用于定义对于包含类来说是私有的类,命名空间中的其他类就不能访问。另一个功能是嵌套类可以访问包含类的私有和受保护成员(原理:嵌套类可以访问包含类的 私有和受包含成员)。
8.3 接口的实现
接口成员定义与类成员的重要区别:
- 不允许使用访问修饰符(
public、private、protected或internal
),所有接口成员都是隐式共用的。 - 接口成员不能包含代码体
- 接口不能定义字段成员
- 不能用关键字
static、virtual、abstract或sealed
来定义接口成员 - 类型定义成员是禁止的
隐藏基接口中继承的成员,与隐藏类成员方式一样
定义属性时可以根据需求省略任意一个
interface IMyInterface
{
int MyInt { get; set;}
}
8.3.1 在类中实现接口
public interface IMyInterface
{
void DoSomething( );
void DoSomthingElse( );
}
public class MyClass : IMyInterface
{
public void DoSomething( ) { };
public void DoSomethingElse( ) { };
}
必须包含该接口所有成员的代码;必须匹配指定的签名(包括匹配指定的get块和set);必须是公开的。
可使用关键字virtual或abstract实现接口成员,但不能使用static或const,还可以在基类上实现接口成员。
继承一个实现给定接口的基类,就意味着派生类隐式支持这个接口。
8.3.2 显式实现接口成员
使用接口名+点句符
public class MyClass : IMyInterface
{
//显式
void IMyInterface.DoSomething( ) { };
//隐式
public void DothingSomeElse( ) { };
}
8.3.3 其他属性存储器
定义接口的存储器是公共的,只能添加非公共的存储器:
public inteface IMyInterface
{
int MyIntProperty { get;}
}
public class MyIntProperface : IMyInterface
{
public int MyIntProperty { get; protected set;}
}
8.4 部分类的定义
1、给代码分组(折叠)
#region
……
#endregion
2、部分类定义
可以将字段、属性、构造函数放在一个文件中,把方法放在另一个文件(就是obj\DeBug这样的文件)中。
语法:
public partial class MyClass { ……}
部分类定义的接口可以应用整个类,即部分类的继承等价于整个类的继承
public partial class MyClass : IMyInterface1 { ……}
public partial class MyClass : IMyInterface2 { ……}
//等价于
public class MyClass : IMyInterface1,IMyInterface { ……}
8.5 部分方法的定义
部分方法定义在一部分类中定义,另一份类中实现,两部分类都需要关键字partial
public partial class MyClass
{
public void MyPartialMethod( );
}
public partial class MyClass
{
public void MyPartialMethod( )
{
// Method impementation.
}
}
部分方法可以是静态的,但总是私有的,不能有返回值(如果有返回值,就可以作为表达式的一部分)。使用的任何参数不能是out,可以是ref。部分方法也不能使用virtual、abstract、override、new、sealed和extern
修饰符。(可以使用“ =>输出字符”)
编译时,如果没有实现部分方法的代码,编译器就会删除这部分,不会影响性能。
9.集合、比较和转换
9.1 集合
集合功能大多是通过System.Collection名称空间的接口获得的
优点:定制的集合类可以是强类型化的(从集合中提取项时,不需要将其转化为正确的类型);提供专用的方法。
9.1.1 集合的基本操作
ArrayList <ListName> = new ArrayList(); //声明+定义:
.add(<elementName>); //添加项
.AddRang(<elementName>); //一次添加好几项
.RemoveAt(<elementIndex>); //删除项
.Remove(<elementName>); //删除项
.Length; //元素个数
.ToString; //元素名称
9.1.2 创建自己的强化集合
-
定义集合
CollectionBase
类有接口IEnumberable、ICollection、IList
,但主要的是IList
的Clear()和RemoveAt()
方法,以及ICollection的Count
属性。CollectionBase
提供的两个受保护属性:
List
可以通过IList
接口访问项。
InnerList
用于存储项的ArrayList
对象。
(他们可以访问存储的对象本身)
//Animal类的派生类是Cow
public class Animals : CollectionBase
{
public void Add(Animal newAnimal)
{
List.Add(newAniaml);
}
public void Remove(Animal oldAnimal)
{
List.Remove(oldAnimal);
}
public Animal() { }
}
//则可以这样调用:
Animals animalCollection = new Animals();
animalCollection.Add(new Cow ("lea"));
- 索引符
也可以通过索引调用(添加索引符):
public class Animals : CollectionBase
{
……
public Animal this(int animalIndex)
{
get { return (Animal)List(animalIndex); }
set { List[animalIndex] = value; }
}
}
//访问时:
animalCollection[0].Tostring();
//而不是:
( (Animal)aniamlCollection).ToString();
-
键控集合和IDictionary
DictionaryBase和CollectionBase
作用一样,但有如下区别:DictionaryBase
不实现MoveAt()
成员区别:
Add()
- 带两个参数:一个键和一个值,实现时使用string
值作为键。
Remove()-
以键为参数,指定键的相应项被删除foreach
工作方式的区别
foreach (DictionaryEntry myEntry in animalCollection)
{
WriteLine( $" New {myEntry.Value.Tostring()} object added to" + $"custom collection, Name = { (Animal)myEntry.Value}.Name");
}
9.1.3 迭代器
是一个代码块,按顺序提供了在foreach
中使用的所有值
语法:yield return <value>
迭代一个类,使用方法GetEnumerator()
,其返回值是IEnumerator
。
迭代一个类成员(例如一个方法),使用IEnumerable
public static IEnumerable SimpleList()
{
yield return "string 1";
yield return "string 2";
yield return "string 3";
}
static void Main(string[ ] args)
{
foreach(string item in SimpleList())
WriteLine(item);
ReadKey();
}
//结果如下:
string 1
string 2
string 3
使用该语句中断将信息返回给foreach循环:yield break;
在后台,调用yield
都会中断代码的执行,当请求一个值时,也就是使用foreach
循环开始一个新循环时,代码会恢复执行
9.1.4 深度赋值
public class Content
{
public int Val;
}
public class clone
{
public Content MyContent = new Content();
public Cloner(int newVal)
{
MyContent.Val = newVal;
}
public object GetCopy() => MemberwiseClone();
}
//会通过GetCopy得到一个浅度复制
Cloner mySource = new Cloner(5);
Cloner myTarget = (Cloner)myCloner.GetCopy();
WriteLine($"myTarget.MyCloner.Val = {myTarget.MyContent.Val}");
mySource.MyContent.Val = 2;
WriteLine($"myTarget.MyCloner.Val = {myTarget.MyContent.Val}");
//此时的输出结果为:
myTarget.MyCloner.Val = 5
myTarget.MyCloner.Val = 2
//如果要实现深度复制,需修改GetCopy方法,如下:
public class cloner : ICloneable
{
public Content MyContent = new Content();
public Cloner(int newVal)
{
MyContent.Val = newVal;
}
public object Clone()
{
Cloner clonedCloner = new Cloner(MyCloner.Val);
retrun clonedClonder;
}
}
继承ICloneable
接口,其中有一个Clone
方法,该方法无参数,有object
返回值,使用时和GetCopy
方法相同
在复杂的系统中,需要进行多次深度复制,调用Clone
就是一个递归的过程(如下面这个两次复制)。
public class cloner : ICloneable
{
public Content MyContent = new Content();
……
public object Clone()
{
Cloner clonedCloner = new Cloner();
clonedCloner.MyContent = MyContent.Clone(); //递归
return clonedCloner;
}
}
9.2 比较
9.2.1 封箱和拆箱
封箱是隐式的,拆箱则需要显示转换。
封箱允许在项是object的时候使用值类型,内部机制允许在值类型上调object方法。
访问值类型内容前,必须先拆箱
9.2.2 值比较
- 运算符重载
public class AddClass1
{
public int Val;
}
AddClass1 op1 = new AddClass1();
op1.Val = 5;
AddClass1 op2 = new AddClass1();
op2.Val = 5;
bool op3 = op2==op3;
上面的代码可以运行,但不会达到预期的结果。比较的是op1和op2是否引用同一对象。
通过等于号重载实现预期的效果,只需要修改类:
public class AddClass1
{
public int Val;
public static bool operator ==(AddClass1 op1, AddClass op2)
{
return ( op1.Val == op2.Val )true : false;
}
}
与静态方法的声明类似,使用关键字operator。
可以重载的运算符:
一元 —— +,-,!,~,++,--,true,false
二元 —— +,-,*,/,%,^,|,<<,>>
比较运算符 —— ==,!==,<,>,<=,>=
有混合类型时,注意操作数的使用顺序。
-
对集合排序
默认比较方式的排序和非默认比较方式的排序
如ArrayList包含的方法Sort。 -
IComparable
接口和IComparer
接口
IComparable
在要比较的类中实现,可以比较该对象和另一个对象
IComparer
在一个单独类中实现,可以比较任意两个对象
IComparable
提供了一个CompareTo方法:<object1>.CompareTo(<object2>)
IComparer
提供了Compare方法:Compare( <object1>, <obbject2>)
两个方法的返回值均是int型。
两种方法提供的参数均为System.Object,在返回结果之前,通常需要类型比较。对简单类型的比较:
Comparer.Default.Compare( <object1>, <object2>)
object1
大则返回1,object2
大则返回-1,一样大返回0。(这里通过Default获取类的实例)
Compare类的注意事项:
1、检查传送给Comparer.Compare( )的对象,看他们是否支持IComparable。如果支持就使用该代码
2、允许使用“null”,它表示小于其他任意对象
3、字符串根据当前文化处理。Comparer必须使用当前构造函数进行实例化以便传送用于指定所使用文化的System.Globalization.CultureInfo对象
4、字符串在处理时要区分大小写。不希望区分时,则需要使用CaseInsensitiveComparer类,该类以相同的方式工作
9.2.3 is
类型转换
<operand> is <type>
表达式结果如下:
- 如果
是一个类类型, 也是该类型,或者它继承了该类型,或者它可以封箱到该类型中,结果为true - 如果
是一个接口类型, 也是该类型,或者它是实现该接口的类型,结果为true - 如果
是一个值类型, 也是该类型,或者它可以拆箱到该类型中,结果为true
9.3 转换
- 重载类型转换符
重载可以进行隐式、显示转换和不相关类型转换。
public class ConvClass1
{
public int Val;
/* 如果认为这部分可以省略,则说明之前的运算符重载部分没有理解,
建议在这里多看几遍*/
public static implicit operator ConvClass2(ConvClass op1)
{
ConvClass2 returnVal = new ConvClass2();
returnVal.Val = op1.Val;
return returnVal;
}
}
public class ConvClass2
{
public double Val;
public static explicit operator ConvClass1(ConvClass op1)
{
ConvClass1 returnVal = new ConvClass1();
checked{ returnVal.Val = (int)op1.Val;}
return returnVal;
}
}
//调用
ConvClass1 op1 = new ConvClass();
op1.Val = 3;
ConvClass2 op2 = op1;
//但反向时会出现一个异常
ConvClass2 op1 = new ConvClass2();
op1.Val = 3e15;
ConvClass1 op2 = (ConvClass2)op1;
使用关键字implicit( 隐式 [默认])和explicit(显式)指定(显隐)转换,使用关键字checked,对整型运算和转换显示启用溢出检查
as
运算符
as运算符一般适用于引用类型转换
<operand> as <type>
只适用于:
<operand>的类型是<type>
<operand>可以隐式转换为<type>类型
<operand>可以封箱到<type>类型中
//如果转换失败,表达式结果为null
class ClassA : IMyInterface()
class ClassD : ClassA()
//示例一,以下结果为:null
ClassA ob1 = new ClassA();
ClassD ob2 = ob1 as (ClassD);
//示例二,以下代码没有异常
ClassD ob1 = new ClassD();
ClassA ob2 = ob1;
ClassD ob3 = ob2 as (ClassA);
//示例三,抛出一个异常
ClassA ob1 = new ClassA();
ClassD ob2 = (ClassD)ob1;
10.泛型
10.1 使用泛型
10.1.1 可空类型
之前我们学习到:值类型只能是值,而引用类型可以是null。为解决某些问题,可以让值类型也使用null。
System.Nullable<int> nullableInt; /* 声明了可空变量nullableInt*/
int? nullableInt; /* 这行代码是上面的缩写,而且更便于读取*/
if( nullableInt.HasVaule) { ……}
/* 可以使用HasValue属性,但引用类型无法做到*/
- 运算符和可空类型
可空类型可以使用自己提供的运算符
//result不是int?类型,所以不会编译
int? op1 = 5;
int result = op1 * 2;
//必须进行显式转换
int result = (int)op1 * 2;
//或通过Value属性访问值
int result = op1.Vaule * 2;
//当运算表达式中有一个或两个是null时,其结果为null
int? op1 = null;
int? op2 = 5;
int? result = op1 * op2;
但是bool?除外,对于bool?,&和 | 的定义运算符都会得到符合逻辑的结果。
??
运算符
空合并运算符
op1 ?? op2; <=>
op1 == null ? op2 : op1;
优点:便捷(不需要if和三元运算符)
?.
运算符
Elvis运算符或空条件运算符
(避免繁杂的空值检查造成歧义)
int count = 0;
if (customer.orders != null)
{
count = customer.orders.Count();
}
//若客户没有订单(null),就会抛出异常
//使用?.运算符会把int? count设置为null
int?. count = customer.orders?.Count();
//也可以设置一个默认值
int? count = customer.orders?.Count ?? 0;
//另一个用途是触发事件
var onChanged = OnChanged;
if ( onChanged != null)
{
onChanged(this, args);
}
/* 该模式不是线程安全的,因为有人会在null检查完成后,
退订最后一个事件处理程序,此时会抛出异常,程序崩溃。
使用空条件可以避免这种情况*/
OnChanged?.Invoke( this, args);
类的==
运算符重载中,使用?.检查null,可以避免使用方法抛出异常:
public static bool operator==( Card card1, Card card2)
=> (card1.suit == card2.suit) && (card1.rank == card2.rank);
如果左边对象card1
不为空,就检索右边对象,否则终止访问链,返回null
。
10.1.2 System.Collections.Generic
命名空间
List<T>
创建T类型对象集合代码:
List<T> myCollection = new List<T>();
//Item属性,允许进行类似于数组的访问
T itemAtIndex = myCollectionOfT[2];
Animals animalCollection = new Animals();
//用泛型集合实现
List<Aniaml> animalCollection = new List<Animal>();
/* Aniaml是之前添加的索引符,可以对照《集合、比较和转换》的章节*/
/*之前添加的索引符代码也可以用下面这行替换*/
public class Animala : List<Animal> { }
11.高级C#技术
转载请注明出处^_^