【游戏开发笔记】编程篇_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,但主要的是IListClear()和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#技术

转载请注明出处^_^

posted @ 2021-12-31 10:37  码农要战斗  阅读(82)  评论(0编辑  收藏  举报