一、类和对象
1.类
类是一个能存储数据并执行代码的数据结构。它包含数据成员和函数成员。
类通过关键字class声明,声明位置在命名空间下。可以将类看作是由程序员来自定义一种新的数据类型。这种类型是引用类型,也就是说有这种类型创建出的变量需要两块内存地址,分别用来存储引用数据和实际数据。
2.对象
类的声明只是类的实例化的蓝图,就是说声明了类就拥有了数据类型,而你想要通过类型去创建实例就需要声明变量并初始化它。通过这个蓝图你可以实例化任意数量个对象,且每个对象都是相互独立、互不干扰的。注意声明变量和初始化变量是两个步骤,它们会分别开辟两个空间。
class Dealer
{
public int age;
}
class Program
{
static void Main( )
{
Dealer theDealer; //仅仅是创建的一个变量,mc中未被初始化,为null
//mc被存储在栈上
theDealer=new Dealer( ); //通过关键字new为实际数据在堆上开辟空间,并返回地址赋值给变量
Dealer theDealer1=new Dealer( ); //theDealer和theDealer1是独立的
}
}
3、访问修饰符
public:类内外均可访问,命名空间下的类默认为public
protected internet:同一程序集或者子类可以访问
internal:同一程序集下可访问
protected:类内及子类可访问
private:只能在类内部访问,如果一个成员没有访问修饰符那它默认为private
4、成员方法
方法是一块具有名称的代码。成员方法必须实例化出对象,通过对象调用方法(不包括静态方法)。
class Person
{
public string name;
public int age;public void Introduce()
{
Console.WriteLine($"{name}今年{age}岁了!");
}
}static void Main()
{
Person p = new Person();
p.name = "Joney";
p.age = 17;
p.Introduce();
}
5、构造函数
如果不写构造函数,会默认有一个无参构造函数
class Person
{
public string name;
public int age;public Person() { }
public Person(string name,int age):this() //只要重载了有参构造函数,那么无参构造函数将会被覆盖 使用需重写
{
this.name = name; //若后面有:表示先调用后面的构造函数
this.age = age;
}
}static void Main()
{Person p1 = new Person("Joney", 17); //调用有参构造函数
}
6、析构函数
当内存被回收时,析构函数才会被调用。开发游戏中基本不用。
7、垃圾回收机制
C#会将堆区(head)分成三个区域,分别为0代内存、1代内存、2代内存。每当程序员用new分配内存时,如果0代内存未满将会在0代内存中开辟空间。如果0代内存满了,将会触发GC(内存回收机制),它会遍历0代内存,将没有索引的空间释放掉,然后将余下的数据迁移到1代内存中。如果需要开辟的内存大于83kb将会直接在1代内存中开辟空间。如果1代内存满了,也会触发GC,它会遍历0代内存及1代内存,将无用的空间释放掉。然后再将1代内存中的数据迁移到2代内存中,0代内存的数据迁移到1代内存中。如果2代内存满了,也会触发GC,它会遍历0代内存、1代内存以及2代内存,将无用的空间释放掉。也可以手动触发GC,通常会在加载场景时手动触发GC。
装箱和拆箱
装箱:用object来存储值类型,把值类型用引用类型存储,栈内存迁移到堆内存中
拆箱:把object中存储的值类型转换为值对象,把引用类型用值类型存储,堆内存迁移到栈内存中
优点:不确定类型时,可以使用object来存储任何类型,方便存储和传递
缺点:装箱拆箱会发生内存迁移,增加性能消耗
int a=5;
object o=a;
a=(int)o;
二、多态
多态,即多种状态。继承同一父类的子类,用里式替换原则在执行相同的方法时均表现为父类的行为。为解决这一问题引入多态概念。多态可以使子类在调用相同的方法时,均表现自己的行为,而不是父类行为。有三中方式可以实现多态,分别为虚函数、抽象函数以及接口。
using System;
namespace C_Pharp
{
class Base
{
public void Printf()
{
Console.WriteLine("正在调用Base类中的Printf方法");
}
}
class Son:Base
{
public new void Printf()
{
Console.WriteLine("正在调用Son类中的Printf方法");
}
}
class Class1
{
static void Main()
{
Base base1=new Son();
base1.Printf(); //用里式替换原则时,执行相同方法均会有相同的表现 子类对象也同样会执行父类中的方法
//里式替换原则:用基类变量来装载派生类对象
(base1 as Son).Printf(); //此时又会表现自身的方法
}
}
}
1、虚函数
虚函数的关键字是virtual,每一个virtual都必须有一个override与之对应。用override修饰的方法会重写用virtual修饰的方法。
using System;
namespace C_Pharp
{
class Base
{
public virtual void Printf()
{
Console.WriteLine("正在调用Base类中的Printf方法");
}
}
class Son:Base
{
public override void Printf()
{
base.Printf(); //关键字base会保留父类的行为,需要父类方法时可以通过base访问
Console.WriteLine("正在调用Son类中的Printf方法");
}
}
class Class1
{
static void Main()
{
Base base1=new Son();
base1.Printf(); //此时会表现为自身的方法
}
}
}
练习:创建图形类,包含求面积和周长的两个方法。创建矩形类、正方形类、圆类继承自图形类,并实现两个方法
using System;
namespace C_Pharp
{
class Diagram
{
public virtual void Area() { }
public virtual void Perimeter() { }
}
class Rectangle:Diagram
{
int length;
int width;
public Rectangle(int length,int width)
{
this.length = length;
this.width = width;
}
public override void Area()
{
Console.WriteLine($"此矩形的面积为:{length*width}");
}
public override void Perimeter()
{
Console.WriteLine($"此矩形的周长为:{2 * length * width}");
}
}
class Square:Diagram
{
int length;
public Square(int length)
{
this.length = length;
}
public override void Area()
{
Console.WriteLine($"此正方形的面积为:{length*length}");
}
public override void Perimeter()
{
Console.WriteLine($"此正方形的周长为:{4 * length}");
}
}
class Circle:Diagram
{
const double PI = 3.1415926;
int radius;
public Circle(int radius)
{
this.radius = radius;
}
public override void Area()
{
Console.WriteLine($"此圆的面积为:{PI*radius*radius}");
}
public override void Perimeter()
{
Console.WriteLine($"此圆的周长为:{2 * PI * radius}");
}
}
class Class1
{
static void Main()
{
Diagram diagram = new Rectangle(3, 4);
diagram.Area();
diagram.Perimeter();
diagram = new Square(3);
diagram.Area();
diagram.Perimeter();
diagram = new Circle(3);
diagram.Area();
diagram.Perimeter();
}
}
}
2、抽象类及抽象方法
关键字:abstract、override
抽象类:不可以被实例化,抽象类被继承时可以用里式替换原则。抽象类被继承时,类中的抽象方法必须被重写。
抽象函数:只能在抽象类中声明,且没有方法体(即没有{})。访问修饰符不能是私有的(private)。
虚方法和抽象方法的区别:
(1)虚方法必须要有方法体,方法体内部可以为空。抽象方法不存在方法体。
(2)虚方法可以被重写,也可以不重写。抽象方法必须要重写。
using System;
namespace C_Pharp
{
abstract class Animal
{
public abstract void Speak(); //抽象方法
}
class Cat : Animal
{
public override void Speak()
{
Console.WriteLine($"小猫在说话!");
}
}
class Class1
{
static void Main()
{
Animal animal = new Cat();
animal.Speak();
}
}
}
练习:同虚函数
using System;
namespace C_Pharp
{
abstract class Diagram
{
public abstract void Area(); //声明两个抽象方法
public abstract void Perimeter(); //与虚函数相比仅仅修改了这两处
}
class Rectangle : Diagram
{
int length;
int width;
public Rectangle(int length, int width)
{
this.length = length;
this.width = width;
}
public override void Area()
{
Console.WriteLine($"此矩形的面积为:{length * width}");
}
public override void Perimeter()
{
Console.WriteLine($"此矩形的周长为:{2 * length * width}");
}
}
class Square : Diagram
{
int length;
public Square(int length)
{
this.length = length;
}
public override void Area()
{
Console.WriteLine($"此正方形的面积为:{length * length}");
}
public override void Perimeter()
{
Console.WriteLine($"此正方形的周长为:{4 * length}");
}
}
class Circle : Diagram
{
const double PI = 3.1415926;
int radius;
public Circle(int radius)
{
this.radius = radius;
}
public override void Area()
{
Console.WriteLine($"此圆的面积为:{PI * radius * radius}");
}
public override void Perimeter()
{
Console.WriteLine($"此圆的周长为:{2 * PI * radius}");
}
}
class Class1
{
static void Main()
{
Diagram diagram = new Rectangle(3, 4);
diagram.Area();
diagram.Perimeter();
diagram = new Square(3);
diagram.Area();
diagram.Perimeter();
diagram = new Circle(3);
diagram.Area();
diagram.Perimeter();
}
}
}
3、接口
关键字:interface
接口是行为的抽象规范,只包含方法、属性、索引器、事件,且成员不能被实现,没有方法体。也就是说接口里面都是行为,没有特征(数据成员)。成员可以不写访问修饰符,默认是public,不可以是私有的。接口不能继承类,一般接口都是被类继承。类可以继承1个类以及n个接口。接口被类继承后必须在类中实现所有的成员。接口的命名以大写I开头。
练习:电脑拥有USB接口,外部存储设备(包括移动硬盘、U盘)可以通过USB接口与电脑进行数据传输
using System;
namespace C_Pharp
{
interface IUSB //USB接口
{
void Transmission(); //传输数据方法
}
class StorageDevice:IUSB //存储设备
{
public string name;
public StorageDevice(string name)
{
this.name = name;
}
public void Transmission() //重写接口中的Transmission方法
{
Console.WriteLine("{0}正在传输数据",name);
}
}
class Computer
{
public IUSB usb;
}
class Class1
{
static void Main()
{
Computer computer = new Computer();
StorageDevice ud = new StorageDevice("U盘");
StorageDevice hd = new StorageDevice("移动硬盘");
computer.usb= ud;
computer.usb.Transmission();
computer.usb = hd;
computer.usb.Transmission();
}
}
}
三、小知识点
1、命名空间
可以将命名空间看做一个工具箱,用来管理类。不同命名空间通过using来引用,引用命名空间后就可以访问该命名空间下的内容。
using System;
using GameObject; //通过using引用命名空间
namespace GameObject
{
class Player
{
public void player()
{
Console.WriteLine("我是玩家");
}
}
class Enemy
{
public void enemy()
{
Console.WriteLine("我是敌人");
}
}
}
namespace C_Sharp
{
class Class1
{
static void Main()
{
GameObject.Player player = new GameObject.Player(); //通过命名空间加.的方式访问命名空间中的类
player.player();
Enemy enemy = new Enemy();
enemy.enemy();
}
}
}
2、object中的方法
虚方法: public virtual string? ToString(),自定义字符串打印规则。当输出一个类对象时,默认会输出该对象的命名空间和类名。
也可以重写虚函数,来自定义规则:
成员方法:public Type GetType(),与反射相关
调用此函数会返回Type类型
成员方法:protected Object MemberwiseClone() ,浅拷贝一个复制体
虚方法:public virtual bool Equals(Object? obj),判断当前对象是否与传入的对象引用同一块内存
静态方法:public static bool Equals(Object? objA, Object? objB),如果是值类型判断两者数据是否相等(微软在值类型的基类System.ValueType中重写了该方法,用来比较值相等),如果是引用类型判断两者是否引用同一块内存。
静态方法:public static bool ReferenceEquals(Object? objA,Object? objB),如果是值类型直接返回false,如果是引用类型判断两者是否引用同一块内存。
虚方法:public virtual int GetHashCode(),获取对象的哈希码。
练习1:声明一个玩家类,包含姓名、血量、攻击力、防御力、闪避率等特征,请输出以上特征
using System;
namespace C_Sharp
{
class Player
{
public string name = "Joney";
public int health = 100;
public int atk = 10;
public int defence = 1;
public float dodge = 0.6f;
public override string ToString() //重写object中的ToString方法
{
return "姓名:" + name + '\n' +
"血量:" + health + '\n' +
"攻击力:" + atk + '\n' +
"防御力:" + defence + '\n' +
"闪避率:" + dodge;
//也可以使用string.Format()方法拼接
}
}
class Class1
{
static void Main()
{
Player player = new Player();
Console.WriteLine(player);
}
}
}
3、string类中的方法
静态方法string.Format(" "),字符串拼接
str.IndexOf(int n),正向查找字符串位置,没找到返回-1
str.LastIndexOf(int n),反向查找字符串位置,没找到返回-1
str.Remove(int n),移除n之后的所有字符(包括n),此函数不会改变原字符
str.Remove(int d1,int d2),移除d1、d2之间的所有字符(包括d1、d2),此函数不会改变原字符
str.Replace(char old,char new),用new去替换old,此函数不会改变原字符
str.ToUpper(string s),小写字母转大写,此函数不会改变原字符
str.ToLower(string s),大写字母转小写,此函数不会改变原字符
str.Substring(int d),字符串截取,截取从d开始后的字符串,此函数不会改变原字符
str.Substring(int d,int n),字符串截取,截取从d开始之后的n个字符串,不能超出界限,否则会报错,此函数不会改变原字符
重点:str.Split(char c),字符串切割,以字符c为标识将字符串分割开来,字符c不会被返回,此函数不会改变原字符
using System;
namespace C_Sharp
{
class Class1
{
static void Main()
{
string str = "I love you";
string[] s = str.Split(' '); //以空格为标识将字符串分割
for(int i=0;i<s.Length;i++)
{
Console.WriteLine(s[i]);
}
}
}
}
4、StringBuilder
每次修改拼接会产生较少的垃圾
查询字符串长度:str.lenght
查询容器容量:str.Capacity
增加字符:str.Append(" ")
替换:str.Replace(char old,char new)
清空:str.Clear
判断字符串是否相等:str.Equals(obj),StringBuilder类里已经重写了Equals方法
如何优化内存:如何节约内存、如何少触发GC
少new对象,合理使用static、合理使用string和StringBuilder
5、结构体和类的区别
结构体是值类型,类是引用类型。所有,结构体存储在栈上,类存储在堆上。
结构体具备封装特性,但不具备继承和多态特性。