虽然项目中一直在使用类、结构体等类型,仔细琢磨,还真无法系统的说出个所以然。记录一下类、结构体、类和结构体区别
一、类
对于类,大家都特别熟悉。简单的介绍一下类的结构,然后记录一下Class需要注意的地方。考虑到静态类和非静态类的区别。下面介绍的都是主要以非静态类为前提介绍。
1、类的成员
类的数据和函数都属于类的成员
(1)类的成员可以分为两类:首先是类本身所声明的。然后是从基类中继承来的。如果在类声明中没有指定基类,则该类将继承System.Object类的所有成员。
(2)C#中的类成员可以是任意类型,包括数组和集合。
(3)C#类成员修饰符。
private声明私有成员,只有该类中的成员可以访问,如果在声明中没有设置访问修饰符,则默认是private。
public 声明公有成员,访问不受限制,允许从外部访问。
protected声明受保护成员,包含类和包含类派生的类可以访问 ,对外界是隐藏的。
internal声明内部成员,只能当前程序集可以访问。
protected internal声明受保护的内部成员,只能访问当前程序集和包含类派生的类。
public class Teacher { private int _age; private string _name; public Teacher(int age, string name) { this.Age = age; this.Name = name ?? throw new ArgumentNullException(nameof(name)); } public int Age { get { return this._age; } set { this._age = value;} } public string Name { get { return this._name; } set { this._name = value; } } public string GetName() { return this.Name; } }
2、类的构造函数
概念:构造函数是类的一种特殊方法,每次创建类的实例都会调用它。在创建一个类的实例时,构造函数就像一个方法一样被调用,但不返回值。
分类:实例构造函数,静态构造函数,私有构造函数。
(1)实例构造函数的特征
a、构造函数的名字与类名相同。
b、使用 new 表达式创建类的对象或者结构(例如int)时,会调用其构造函数。并且通常初始化新对象的数据成员。
c、除非类是静态的,否则会为没有构造函数的类,自动生成一个默认构造函数,并使用默认值来初始化对象字段。
d、构造函数可以有参数,可以以多态的形式存在多个构造函数。
class Program { static void Main(string[] args) { Car car = new Car(); car.Print(); Car car1 = ,"奥迪"); car1.Print(); Console.ReadKey(); } } public class Car { private int _price; private string _carName; public Car() { ; this.CarName = "奔驰"; } public Car(int price,string carName) { this.Price = price; this.CarName = carName; } public int Price { get { return this._price; } set { this._price = value; } } public string CarName { get { return this._carName; } set { this._carName = value; } } public void Print() { Console.WriteLine("{0}价格是{1}元!",this.CarName,this.Price); } }
(2)静态构造函数的特征
a、静态构造函数不使用访问修饰符或不能有参数。
b、不能直接调用静态构造函数。
c、如果静态构造函数引发异常,运行时将不会再次调用该函数,并且类型在程序运行所在的应用程序域的生存期内将保持未初始化。
d、用户无法控制在程序中执行静态构造函数的时间。
class Program { ; public Program() { _price = ; } static Program() { _price += ; } static void Main(string[] args) { Console.WriteLine("房子价格{0}",_price);//程序启动,静态类就会占用内存,会先执行静态构造函数 _price=1001 Program program = new Program(); Console.WriteLine("房子价格{0}",_price);//实例化类,会执行实例构造函数 _price=10 Console.ReadKey(); } }
在调用某类的静态函数时真正的执行顺序:
1、静态变量 > 静态构造函数 > 静态函数
2、静态变量 > 静态构造函数 > 构造函数
public class A { public static readonly int x; static A() { //第二步,调用B.y,此处B.y = 0,因为int类型在初始化阶段,会给赋默认值,默认值为0。最后x = 0 + 1(返回给第一步) x = B.y + ; } } public class B { //第一步,调用A.x,然后执行类A的静态构造函数,等待返回(第二步返回的A.x = 1,所有y = 1 + 1) ; public static void Main(string[] args) { //第三步,A.x = 1,y = 2。 Console.WriteLine("x:{0},y:{1}。", A.x, y); Console.ReadLine(); } } 练习题答案
(3)私有构造函数
私有构造函数是一种特殊的实例构造函数。 它通常用于只包含静态成员的类中。 如果类具有一个或多个私有构造函数而没有公共构造函数,则其他类(除嵌套类外)无法创建该类的实例。声明空构造函数可阻止自动生成默认构造函数。 请注意,如果不对构造函数使用访问修饰符,则在默认情况下它仍为私有构造函数。 但是,通常会显式地使用 private 修饰符来清楚地表明该类不能被实例化。 其中单例模式就用到了私有构造函数的特性来保证类不会被实例化。
class Program { private Program() { // House house = new House();//注释打开会报错,错误信息:不可访问,因为它受保护级别限制。因为私有构造函数无法在类的外面实例化。 } public class House { private int price; private House() { price = ; } static void Main(string[] args) { Program program = new Program();//嵌套类允许实例化 House house = new House(); Console.WriteLine("price{0}",house.price);//输出1000 Console.ReadKey(); } } }
3、类的析构函数
(1)概念:析构函数(destructor) 与构造函数相反,当对象脱离其作用域时(例如对象所在的函数已调用完毕),系统自动执行析构函数。析构函数往往用来做“清理善后” 的工作(例如在建立对象时用new开辟了一片内存空间,应在退出前在析构函数中用delete释放)。
(2)特征
一个类只能有一个析构函数。
不能在结构中定义析构函数。 只能对类使用析构函数。
无法继承或重载析构函数。
无法调用析构函数。 它们是被自动调用的。
析构函数既没有修饰符,也没有参数。
(3)析构函数的作用
程序员无法控制何时调用析构函数,因为这是由垃圾回收器决定的。 垃圾回收器检查是否存在应用程序不再使用的对象。 如果垃圾回收器认为某个对象符合析构,则调用析构函数(如果有)并回收用来存储此对象的内存。 程序退出时也会调用析构函数。
通常,与运行时不进行垃圾回收的开发语言相比,C# 无需太多的内存管理。 这是因为 .NET Framework 垃圾回收器会隐式地管理对象的内存分配和释放。 但是,当应用程序封装窗口、文件和网络连接这类非托管资源时,应当使用析构函数释放这些资源。 当对象符合析构时,垃圾回收器将运行对象的 Finalize 方法
(4)实例
class Program { static void Main(string[] args) { RedApple redApple = new RedApple(); } } class Fruits //基类Fruits { ~Fruits()//析构函数 { Console.WriteLine("~Fruits()的析构函数"); } public Fruits() { Console.WriteLine("Fruits()的构造函数"); } } class Apple:Fruits { ~Apple() { Console.WriteLine("~Apple()的析构函数"); } public Apple() { Console.WriteLine("Apple()的构造函数"); } } class RedApple:Apple { ~RedApple() { Console.WriteLine("~RedApple()的析构函数"); } public RedApple() { Console.WriteLine("RedApple()的构造函数"); } }
运行结果
总结:
1>程序运行时,这三个类的析构函数将自动被调用,调用顺序是按照从派生程度最大的(~C())到派生程度最小的(~A())次序调用的,和构造函数的调用顺序正好相反。则可以得出,当用户创建类的对象是调用构造函数,而当该对象已经调用完毕时,使用析构函数
2>析构函数(destructor) 与构造函数相反,当对象脱离其作用域时(例如对象所在的函数已调用完毕),系统自动执行析构函数。
3>析构函数往往用来做“清理善后” 的工作(例如在建立对象时用new开辟了一片内存空间,应在退出前在析构函数中用delete释放).
4>析构函数名也应与类名相同,只是在函数名前面加一个波浪符~,例如~A( ),以区别于构造函数。它不能带任何参数,也没有返回值(包括void类型)。只能有一个析构函数,不能重载。
5>如果用户没有编写析构函数,编译系统会自动生成一个缺省的析构函数,它也不进行任何操作。所以许多简单的类中没有用显式的析构函数。
4、类的继承
(1)类的注意事项
1>注意静态类和非静态类的特征以及使用方式的不同。可以参见C#基础知识之静态和非静态
2>派生类只能从一个类中继承,从多个基类中派生一个类这往往会带来许多问题,从而抵消了这种灵活性带来的优势。
3>C#中,派生类从它的直接基类中继承成员:方法、域、属性、事件、索引指示器。除了构造函数和析构函数,派生类隐式地继承了直接基类的所有成员。
(2)类的重写 override和virtual
1>重写父类的方法要用到override关键字(具有override关键字修饰的方法是对父类中同名方法的新实现)。
2>要重写父类的方法,前提是父类中该要被重写的方法必须声明为virtual或者是abstract类型。给父类中要被重写的方法添加virtual关键字表示可以在子类中重写它的实现。
3>virtual关键字用于将方法定义为支持多态,有virtual关键字修饰的方法称为“虚拟方法”。
4>virtual:不是必须被子类重写的方法,父类必须给出实现,子类可以重写(使用override,new,或无特殊标志的普通方法),也可以不重写该方法。
5>new:重写父类方法时,父类可以使用virtual,override,new之一声明,也可以是没有关键字的普通方法,运行时会根据引用类型选择调用父类还是子类方法,重写父类方法时,使用new关键字与使用没有关键字的普通方法的等效的,但是后者会给出编译警告。
class Program { static void Main(string[] args) { Dog dog = new Dog("狗"); dog.ShowName(); Console.ReadKey(); } } class Animal { private string _name; public string Name { get { return _name; } set { _name = value; } } public virtual void ShowName() { Console.WriteLine("动物列表:"); } } class Dog : Animal { public Dog(string name) { base.Name = name; } public override void ShowName() { base.ShowName(); Console.WriteLine("动物的名称:{0}",base.Name); } }
class Program { static void Main(string[] args) { Dog dog = new Dog("狗"); dog.ShowName(); Console.ReadKey(); } } abstract class Animal { private string _name; public string Name { get { return _name; } set { _name = value; } } public abstract void ShowName(); } class Dog : Animal { public Dog(string name) { base.Name = name; } public override void ShowName() { Console.WriteLine("动物的名称:{0}",base.Name); } }
(3)类的重载
方法名相同,但是参数不同,参数的个数不同或者类型不同,满足一个就可以(和返回值无关,和参数的类型和个数有关)。注意:返回值可以相同也可以不同。当参数个数相同而参数类型不同的时候,可以考虑使用泛型(C#基础知识之泛型),提高代码的复用性。
class Dog { public void Show() { } public void Show(string name) { } public void Show(string name,int weight) { } public void Show(int weight,int age) { } }
(4)抽象类
修饰类名为抽象类,修饰方法为抽象方法。如果一个类为抽象类,则这个类只能是其他某个类的基类。抽象方法在抽象类中没有函数体。抽象类中的抽象方法是没有方法体的,继承其的子类必须实现抽象类的抽象方法。
1>抽象类的特征
- 抽象类不能实例化。
- 抽象类的派生类必须实现所有抽象方法。
- 抽象类的抽象方法是没有方法体的,继承抽象类的子类必须得实现所有抽象方法。
2>抽象方法的特征
- 抽象方法是隐式的虚方法。
- 只允许在抽象类中声明抽象方法。
- 抽象方法在抽象类中没有方法体。
- 在抽象方法声明中,不能使用static和virtual修饰符。
(5)密封类
密封类不能被继承,所以sealed和abstract不能共用。
1>密封类
class Program { static void Main(string[] args) { B b = new B(); b.x = ; b.y = ; Console.WriteLine("b.x {0}, b.y {1}",b.x,b.y); Console.ReadKey(); } } public sealed class B { public int x; public int y; }
2>密封方法
对类中的方法可以使用sealed修饰符,我们称该方法为密封方法。不是类的每个成员方法都可以作为密封方法,密封方法必须对基类的虚方法进行重载,提供具体的实现方法。所以,在方法的声明中,sealed修饰符总是和override修饰符同时使用,sealed修饰符位于override修饰符前面。
class Program { static void Main(string[] args) { A a1 = new A();//实例化A类的对象,调用A类中的方法 a1.Fun1(); a1.Fun2(); A a2 = new B();//实例化B类的对象,调用B类中的方法,注意前边 new作为修饰符的用法(很重要!) a2.Fun1(); a2.Fun2(); A a3 = new C();//实例化C类的对象,调用C类中的方法,因为FUN1()在B类中使用密封修饰符,所以C类中无法进行重写,所以调用B.Fun1() a3.Fun1(); a3.Fun2(); B b1 = new B();//实例化B类的对象,调用B类中的方法 b1.Fun1(); b1.Fun2(); B b2 = new C();//实例化C类的对象,因为FUN1()在B类中使用密封修饰符,所以C类中无法进行重写,所以调用B.Fun1() b2.Fun1(); b2.Fun2(); C c1 = new C();//因为FUN1()在B类中使用密封修饰符,所以C类中无法进行重写,所以调用B.Fun1() c1.Fun1(); c1.Fun2(); Console.ReadKey(); } } public class A { public virtual void Fun1() { Console.WriteLine("base.Fun1();"); } public virtual void Fun2() { Console.WriteLine("base.Fun2();"); } } public class B:A { public sealed override void Fun1() { Console.WriteLine("B.Fun1();"); } public override void Fun2() { Console.WriteLine("B.Fun2();"); } } public class C : B { public override void Fun2() { Console.WriteLine("C.Fun2()"); } }
(6)New new的关键词有三个功能
- 作为运算符: 用于创建对象和调用构造函数。 Class obj = new Class();
- 作为修饰符 : 在用作修饰符时,new 关键字可以显式隐藏从基类继承的成员。具体地说,new声明的方法,当使用子类的类型来调用的时候,它会运行子类的函数,而如果类型是基类的话,被隐藏的基类函数会被调用。而子类中函数使用override的时候,则当使用子类的类型来调用的是,它会运行子类的函数,类型是基类的时候,仍会调用子类函数。
class Program { static void Main(string[] args) { A a1 = new A();//如果类型是基类的话,被隐藏的基类函数会被调用 a1.ShowInfo(); System.Console.WriteLine("----------"); A a2 = new B(); a2.ShowInfo();//如果类型是基类的话,被隐藏的基类函数会被调用 System.Console.WriteLine("----------"); B a3 = new B(); a3.ShowInfo();//如果类型是子类的话,它会运行子类的函数 System.Console.WriteLine("----------"); A a4 = new C();//子类中函数使用override的时候,类型是基类的时候,仍会调用子类函数。 a4.ShowInfo(); System.Console.WriteLine("----------"); C a5 = new C(); a5.ShowInfo();//子类中函数使用override的时候,则当使用子类的类型来调用的是,它会运行子类的函数 System.Console.WriteLine("----------"); Console.ReadKey(); } class A { public virtual void ShowInfo() { System.Console.WriteLine("Four wheels and an engine."); } } class B : A { public new void ShowInfo() { System.Console.WriteLine("A roof that opens up."); } } class C : A { public override void ShowInfo() { System.Console.WriteLine("Carries seven people."); } } }
- 作为约束: 用于在泛型声明中约束可能用作类型参数的参数的类型。new约束指定泛型类声明中的任何类型参数都必须具有公共的无参数构造函数
class Program { static void Main(string[] args) { AFactory<A> aFactory = new AFactory<A>(); //此处编译器会检查Employee是否具有公有的无参构造函数。 //若没有则会有The Employee must have a public parameterless constructor 错误。 Console.WriteLine("{0}'ID is {1}.", aFactory.GetName().Name, aFactory.GetName().ID); Console.ReadKey(); } } public class A { private string name; private int id; public A() { name = "dachong"; id = ; } public A(string s, int i) { name = s; id = i; } public string Name { get { return name; } set { name = value; } } public int ID { get { return id; } set { id = value; } } } class AFactory<T> where T : new() { public T GetName() { return new T(); } }
二、结构体
1、概念:C# 中,结构体是值类型数据结构。它使得一个单一变量可以存储各种数据类型的相关数据。struct 关键字用于创建结构体。结构体是用来代表一个记录。
2、特点:C#中结构体的特点
(1)结构可带有方法、字段、索引、属性、运算符方法和事件。
(2)结构可定义构造函数,但不能定义析构函数。但是,您不能为结构定义默认的构造函数。默认的构造函数是自动定义的,且不能被改变。
(3)与类不同,结构不能继承其他的结构或类。
(4)结构可实现一个或多个接口。
(5)结构成员不能指定为 abstract、virtual 或 protected。
(6)当您使用 New 操作符创建一个结构对象时,会调用适当的构造函数来创建结构。与类不同,结构可以不使用 New 操作符即可被实例化。如果不使用 New 操作符,只有在所有的字段都被初始化之后,字段才被赋值,对象才被使用。
struct Books { private string title; private string author; private string subject; private int book_id; public void getValues(string t, string a, string s, int id) { title = t; author = a; subject = s; book_id =id; } public void display() { Console.WriteLine("Title : {0}", title); Console.WriteLine("Author : {0}", author); Console.WriteLine("Subject : {0}", subject); Console.WriteLine("Book_id :{0}", book_id); } }; class Program { Books Book1 = new Books(); Book1.getValues("C Programming", ); Book1.display(); Console.ReadKey(); }
三、类和结构体区别
1、结构是值类型,它在栈中分配空间;而类是引用类型,它在堆中分配空间,栈中保存的只是引用。
2、C# 中的简单类型,如int、double、bool等都是结构类型。如果需要的话,甚至可以使用结构类型结合运算符运算重载,再为 C# 语言创建出一种新的值类型来。由于结构是值类型,并且直接存储数据,因此在一个对象的主要成员为数据且数据量不大的情况下,使用结构会带来更好的性能。因为结构是值类型,因此在为结构分配内存,或者当结构超出了作用域被删除时,性能会非常好,因为他们将内联或者保存在堆栈中。当把一个结构类型的变量赋值给另一个结构时,对性能的影响取决于结构的大小,如果结构的数据成员非常多而且复杂,就会造成损失。
3、在结构体中可以声明字段,但是声明字段的时候是不能给初始值的。
4、类中如果我们没有为类写任意的构造函数,那么C#编译器在编译的时候会自动的为这个类生成1个无参数的构造函数.我们将这个构造函数称之为隐式构造函数 但是一旦我们为这个类写了任意的1个构造函数的时候,这个隐式的构造函数就不会自动生成了。隐式的无参数的构造函数在结构中无论如何都是存在的,所以程序员不能手动的为结构添加1个无参数的构造函数,否则会报错。
5、C#语法规定在结构体的构造函数中,必须要为结构体的所有字段赋值。注意在结构体的构造函数中我们为属性赋值,不认为是在对字段赋值,所以我们在构造函数中要直接为字段赋值.
四、类和结构体的使用场所
1、当堆栈的空间很有限,且有大量的逻辑对象时,创建类要比创建结构好一些;
2、对于点、矩形和颜色这样的轻量对象,假如要声明一个含有许多个颜色对象的数组,则CLR需要为每个对象分配内存,在这种情况下,使用结构的成本较低;
3、在表现抽象和多级别的对象层次时,类是最好的选择,因为结构不支持继承。