暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

C# 混淆概念

原创 yBmZlQzJ 2023-09-17
120

Cover

Table of Contents

前言

第 1 章 C#中一些易混淆概念总结-数据类型存储位置,方法调用,out和ref参数的使用

第 2 章 构造函数,this关键字,部分类,枚举

第 3 章 结构,GC回收,静态成员,静态类

第 4 章 解析Console.WriteLine()

第 5 章 继承

第 6 章 解析里氏替换原则,虚方法

第 7 章 解析抽象类,抽象方法

第 8 章 解析接口

前言

本文主要面向 C#基础知识学习者, 作者通过亲身学习与调查, 整理出初学者容易理解不清楚和混淆的概念。

适用人群

本教程是面向正准备学习C#基础知识学习者。本教程可帮助你解决在学习过程中遇到的理解不清楚和混淆的概念。

致谢
内容撰写:http://my.csdn.net/yisuowushinian

更新日期

更新内容

2015-06-1

C#中一些易混淆概念总结

1

C#中一些易混淆概念总结-数据类型存储位置,方法调用,out和ref参数的使用

这几天一直在复习C#基础知识,过程中也发现了自己以前理解不清楚和混淆的概念。现在给大家分享出来我的笔记:

一,.NET平台的重要组成部分都是有哪些

1)FCL (所谓的.NET框架类库)

这些类是微软事先定义好的。

例如当我们新创建一个windows窗体应用程序是,VS会帮我们自动生成下面的代码:

using System;
using System.Collections.Generic;
using System.Text;

这些就是微软事先为程序员定义好的类库。程序员可以直接使用的。

2)CLR (所谓的公共语言运行时)

创建部署.NET程序的必备环境

使用C#,F#,VB等语言都可以来创建.NET应用程序。这时候就需要一个公共语言规范,来把不同的语言解释成.NET FramWork认识的东西。

二,什么是程序集

程序集主要有MSIL组成(所谓的微软中间语言,主要由dll文件组成)

不同编程语言程序被.NET FrameWork编译成程序集(dll文件),当程序需要被执行时,通过CLR中的JIT(及时编译器)编译成本地代码,并将指令发送给CPU执行。

程序集一般有两种:.dll和.exe文件(但是不是所有的dll和exe都叫程序集)

比如:我们在同一个解决方案下,建立多个应用程序或者类库文件。这些程序或者类库编译后就会变成不同的程序集。他们之间是相互独立的。之间如果想要相互访问,需要添加引用。

三,Parse转换和Convert转换的区别

1)Parse转换

①Parse转换只能转换字符串

②自变量是指定的数据类型才可以转换成功

下面的是.NET Reflector编译的源代码

1-1.png

2)Convert转换

①可以转换其他类型(如:类)

②与Parse的区别就是,转换前会对被转换的对象进行判断,如果对象为null则会转换失败

1-2.png

下面是实例源代码:

class Program
{
static void Main(string[] args)
{
string a = Console.ReadLine();
//Parse只可以转换字符串
int b = Int32.Parse(a);

//Convert可以转换类等对象
ParseNumber parNum = new ParseNumber();
//这种写法编译器会报错
//int b = Int32.Parse(parNum);
int c = Convert.ToInt32(parNum);

Console.WriteLine(b);
Console.WriteLine(b.GetType());

Console.ReadKey();
}
}

class ParseNumber
{
private int nunm;
public int Num { get; set; }
}

四,数据类型的存储位置

1)存储在栈中的数据类型

所有数值类型,char,bool,枚举,结构体

2)存储在堆中

string,数组,类

管这些类型,他们的变量的声明都是保存在栈里,真实的对象保存在堆里面,栈里面的变量存储打的是对象的地址。

下面以数组来简单说一下这个问题:

       //声明一个一维数组
int[] arr = new int[4];

那么这个表达式的执行顺序是什么呢?

①首先程序会在栈中开辟一段名为arr的int[]类型的空间

②然后在堆中开辟一个int[]对象,再该对象中会有4块连续的内存空间

③堆中的对象返回类型为地址,即new int[4]表达式返回的是地址

示意图如下:

1-3.png

五,C#方法调用

1)在C#中我们可以给参数传递默认值,所以当我们调用这个方法的时候,可以不给这个参数传递值

static void Main(string[] args)
{
//声明一个一维数组
int[] arr = new int[4];
Program pro = new Program();
       //直接调用,没有传递参数值
pro.para();

}
public void para(int i=5)
{
Console.WriteLine(i);
Console.ReadKey();
}

2)带默认参数的方法,默认值必须放在最右侧

下面的写法编译器会报错

1-14.png

3)方法的可变参数

①可变参数被Params

②Params只能用来修饰一维数组

static void Main(string[] args)
{
//声明一个一维数组
int[] arr = new int[4];

for (int i = 0; i < arr.Length; i++)
{
arr[i] = i;
}
Program pro = new Program();
pro.para();
//传递一位数组arr
pro.param(arr);

Console.ReadKey();
}
//params用来修饰一维数组
public void param(params int[] arr)
{
foreach (var item in arr)
{
Console.WriteLine(item);
}
}

③给可变参数赋值的时候可以直接传递数组元素

       //声明一个一维数组
int[] arr = new int[4];

for (int i = 0; i < arr.Length; i++)
{
arr[i] = i;
}
Program pro = new Program();
pro.para();
//传递一位数组arr
pro.param(arr);
//直接传递数组元素,调用时会自动将这些数封装成数组,并将数组传递
pro.param(0, 1, 2, 3);

Console.ReadKey();

④与默认参数一样,可变参数的声明必须放在方法参数的最后

1-4.png

4)方法的out和ref参数

①out参数侧重于输出,必须在方法内对其赋值

如下图的声明编译器会报错

1-5.png

正确的使用方法

static void Main(string[] args)
{
//声明参数m
int m=0;
Program pro = new Program();
//传递参数m,必须带有out参数标识
pro.outPara( out m);
Console.WriteLine(m);
Console.ReadKey();
}

//out参数侧重于输出,必须在方法内对其赋值
public void outPara(out int i)
{
//方法内部必须对out参数进行赋值
i=5;
}

②ref参数侧重于修改,但是也可以不修改参数的值

static void Main(string[] args)
{
//声明参数m
int m=0;
Program pro = new Program();
//传递参数m,必须带有out参数标识
pro.outPara( out m);
       //ref参数传递之前必须对其赋值,因为在方法内部可能会用到该参数
pro.refPara(ref m);
//Console.WriteLine(m);
Console.ReadKey();
}

//
public void refPara(ref int i)
{
Console.WriteLine("可以不对参数i进行任何操作!");
}

输出结果如下:

1-13.png

六,属性易混淆点辨别

①属性本身不存值,值是存在这个属性所封装的字段里面

class Study
{
private int nID;

//属性的值存储在封装的字段里面
public int NID
{

get { return nID; }
//这里我们给属性赋值
set { nID = value; }
}
}

通过访问属性字段获取字段的值

       Study stu = new Study();
//通过访问属性字段获取字段的值
int nID = stu.NID;

②属性的返回值类型和字段的值类型没有关系

     //属性的值类型为bool
private bool gender;
//字段的返回类型为string
public string Gender
{
get{return gender==true?"男":"女";}
set{gender =value=="男"?true:false;}
}

属性的返回值类型决定了get返回值的类型和set参数的类型

     //属性的值类型为bool
private bool gender;
//字段的返回类型为string
public string Gender
{
//get的返回值类型为bool
get{return gender==true?"男":"女";}
//set参数类型为bool
set{gender =value=="男"?true:false;}
}

③自动属性到底是怎么回事?

看如下的代码:

     private string strName;
//自动属性封装strName
public string StrName
{
get;
set;
}

这就是所谓的自动属性封装字段。在非自动属性中,程序默认的会有value值来给字段赋值,但是在自动属性中是怎么赋值的呢?

我们使用.NET Reflector反编译来看源代码:

这是我们封转的属性代码:

1-7.png

反编译set函数源代码:

1-8.png

我们可以看到.NET会默认为我们的程序生成一个成员变量k__BackingField

get函数的源代码:

1-9.png

返回的也是该成员变量;

那么什么时候可以使用自动属性呢?

如果对一个字段取值和赋值的时候没有任何逻辑验证并且可读可写的时候,就可以使用自动属性。

七,C#类声明易混淆知识点

①首先给大家说明一个问题就是,文件名和类名必须是一样的么(就是我们在创建类的时候要命明,这个时候会默认的生成一样的类名称)?

如图所示

1-10.png

这个是必须的么?

我们尝试修改类名称为ChildName,然后访问类

1-11.png

可以看到我们要访问类,需要通过类名称访问而与文件名没有关系。

②类表达式的执行顺序和其意义

编译器执行代码的时候,

首先会先在栈中开辟一块类型为Study的内存空间放置变量stu

然后在堆中创建该变量的对象

然后调用该对象的构造函数,并且返回该对象在堆中的地址。

1-12.png

好吧,到这里,这次的分享就到此结束了。大家如果阅读的过程中有什么问题,可以跟我留言交流。

2

构造函数,this关键字,部分类,枚举

目录:

【C#小知识】C#中一些易混淆概念总结--------数据类型存储位置,方法调用,out和ref参数的使用

继上篇对一些C#概念问题进行细节的剖析以后,收获颇多。以前,读书的时候,一句话一掠而过,但是现在再去重读的时候,每句话发现都包含大量的信息。这一篇继续总结自己的学习笔记,给大家深度的剖析一些概念性问题,有助于大家对C#的理解。

一,构造函数

我们先创建一个类,如下面的代码:

class Program
{
static void Main(string[] args)
{
       
}
}
  //创建一个Person类
class Person
{

}

然后生成代码。

我们使用.NET Reflector反编译该程序集。会发现该类一被编译,CLR会自动的为该类创建一个默认的构造函数。如下图:

2-1.png

所以在创建该对象的时候,会默认的为该类生成一个无参数的空方法体的构造函数。如果我们不显式的写明构造函数,CLR会为我们调用默认的构造函数。

class Person
{
//声明有实现的构造函数
public Person()
{
Console.WriteLine("我是超人!");
}
}

再次反编译该程序集,会发现添加的构造函数覆盖了C#编译器默认为该类生成的构造函数,如下图:

2-2.png

所以,当程序员手动添加了任意类型的构造函数,C#编译器就不会为该类添加默认的构造函数。

构造函数的特点:

①访问修饰符一般是Public②没有返回值,方法名与类名称一致;

二,This关键字的作用

①this关键字代表当前对象,当前运行在内存中的那一个对象。我们添加如下的代码:

private int nAge;

public int NAge
{
get { return nAge; }
set { nAge = value; }
}

//声明有实现的构造函数
public Person()
{
this.NAge = 100;
Console.WriteLine("我是超人!");
}

这时候我们反编译该程序集,会看到如下结果:

2-3.png

可以看到this关键字代替的就是当前的Person对象。

②this关键字后面跟":"符号,可以调用其它的构造函数

我们再添加如下的代码:

#region 对象的构造函数
//声明有实现的构造函数
public Person()
{
this.NAge = 100;
Console.WriteLine("我是超人!");
}

public Person(int nAge)
{
Console.WriteLine("超人的年龄{0}", nAge);
}
     //使用this关键字调用了第二个一个参数的构造函数
public Person(int nAge, string strName)
: this(1)
{
Console.WriteLine("我是叫{0}的超人,年龄{1}", strName, nAge);
}
#endregion

我们创建该对象看看是否调用成功。在Main函数中添加如下代码:

Person p = new Person(10,"强子");

我们运行代码,看到的打印结果如下:

2-4.png

由结果我们可以分析出,当含有两个默认参数的对象创建的时候应该先调用了一个参数的构造函数对对象进行初始化,然后有调用了含有两个参数的构造函数对对象进行初始化。

那么到底是不是这个样子呢?看下边的调试过程:

2-5.gif

通过上面的调试过程我们会发现,当构造函数使用this关键字调用其它的构造函数时,首先调用的是该调用的构造函数,在调用被调用的构造函数,先执行被调用的构造函数,在执行直接调用的构造函数。

为什么要这个顺序执行?因为我们默认的传值是10,我们需要打印的超人的年龄是"10",如果先执行直接调用的构造函数,就会被被调用构造函数覆盖。

三,部分类

在同一命名空间下可以使用partial关键字声明相同名称的类(同一命名空间下默认不允许出现相同的类名称),叫做部分类或者伙伴类。

如下图,当在同一命名空间下声明相同名称的类,编译器报错:

2-6.png

当我们使用Partial关键字时,可以顺利编译通过,如下图:

2-7.png

分别添加如下的代码:

partial class Person
{
private string strAddress;

public string StrAddress
{
get { return strAddress; }
set { strAddress = value; }
}
private string strNumber;

public string StrNumber
{
get { return strNumber; }
set { strNumber = value; }
}

public void Run()
{

}
}

partial class Person
{

#region 对象属性
private int nAge;

public int NAge
{
get { return nAge; }
set { nAge = value; }
}

private string strName;

public string StrName
{
get { return strName; }
set { strName = value; }
}

#endregion

#region 对象的构造函数
//声明有实现的构造函数
public Person()
{
this.NAge = 100;
Console.WriteLine("我是超人!");
}

public Person(int nAge)
{
Console.WriteLine("超人的年龄{0}", nAge);
}

public Person(int nAge, string strName)
: this(1)
{
Console.WriteLine("我是叫{0}的超人,年龄{1}", strName, nAge);
}
#endregion

public void Sing()
{

}
}

我们再次反编译该程序集,会发现如下的结果:

2-8.png

我们会发现使用Partial关键字的两个同名类,被编译成了同一个类。

所以部分类的特点:

①必须在同一个命名空间下的使用Partial关键字的同名类

②部分类其实就是一个类,C#编译器会把它们编译成一个类

③在一个伙伴类中定义的变量可以在另一个伙伴类中访问(因为他们就是一个类)。

四,Const关键字和Readonly关键字的区别

1)const关键字

在Main函数中添加如下的代码:

       const string strName = "强子";
Console.WriteLine("我的名字叫{0}",strName);

编译过后,我反编译该程序集发现如下结果:

2-9.png

发现定义的常量并没有出现在反编译的代码中,而且使用Const常量的地方被常量代替了。

2)readonly关键字

添加如下代码:

class cat
{
readonly string reOnlyName = "强子";
public cat()
{
Console.WriteLine(reOnlyName);
}
}

生成后反编译该程序集发现,如下结果:

2-10.png

我们发现被readonly修饰的变量并没有被赋值,这是什么回事呢?我们点击cat类的构造函数时,看到如下结果:

2-11.gif

我们发现被readonly修饰的变量是在被调用的时候赋值的。

那么被readonly修饰的变量的是就是不可变的么?当然不是,由反编译的结果我们知道,readonly修饰的变量是在被调用的时候在构造函数中被赋值的,那么我们可以在构造函数中修改readonly的默认值

添加如下代码:

class cat
{
readonly string reOnlyName = "强子";
public cat()
{
this.reOnlyName = "子强";
Console.WriteLine(reOnlyName);
}
}

在Main()函数中添加如下的代码:

运行结果如下:

2-12.png

说明我们成功在构造函数中修改了readonly变量的值。

readonly和const的区别:

const常量在声明的时候就必须赋初始值,这样声明变量可以提高程序的运行效率。而readonly变量声明时可以不赋初始值,但一定要早构造函数中赋初始值。

也就是说,const变量在编译的时候就要确定常量的值,而readonly是在运行的时候确定该变量的值的。

** 五,解析枚举**

枚举的级别和类的级别一样,可以自定义数据类型,可以在枚举名称后使用":"来指明枚举类型。看如下代码:

//定义一个方向的枚举类型,枚举成员使用","分割
enum Direction:string
{
east,
west,
south,
north
}

编译会报错,错误信息如下:

2-13.png

由此我们可以知道枚举的数据类型是值类型。

因为枚举是数据类型,所以可以直接声明访问,如下代码:

class Program
{
static void Main(string[] args)
{
//枚举是数据类型可以直接声明
Direction dr = Direction.east;

Console.WriteLine(dr);

Console.ReadKey();
}
}

//定义一个方向的枚举类型,枚举成员使用","分割
enum Direction
{
east,
west,
south,
north
}

也可以这样访问枚举类型

class Program
{
static void Main(string[] args)
{
//枚举是数据类型可以直接声明
// Direction dr = Direction.east;

Person p=new Person();
//直接调用枚举变量
p.dir = Direction.east;
Console.WriteLine(p.dir);

Console.ReadKey();
}
}

class Person
{
private string strName;
//直接声明枚举变量
public Direction dir;
}

每一个枚举成员都对应了一个整型的数值,这个数值默认从0开始递增,可以通过强制转换获取该枚举所代表的值。可以通过如下的代码访问:

       Direction dr = Direction.east;
int i = (int)dr;

我们还可以手动为每一个枚举成员赋值,代表的是整型数值,赋值后该枚举成员所代表的值就是所赋的值。如下代码:

enum Direction
{
east=1,
west=0,
south=2,
north=3
}

将字符串转换成枚举

       string strDir = "east";
//将字符串转换成枚举类型
Direction d1=(Direction)Enum.Parse(typeof(Direction),strDir);
//转换的时候忽略大小写
Direction d2 = (Direction)Enum.Parse(typeof(Direction), strDir,true);

--------------------------------分割线----------------------------------------

最后我们再来探究一个空指针异常的问题

首先我们先声明一个Dog类:

class Dog
{
private int nAge;

public int NAge
{
get { return nAge; }
set { nAge = value; }
}
private string strName;

public string StrName
{
get { return strName; }
set { strName = value; }
}
}

在Main()函数中我们这样调用

       Dog d = null;
d.StrName = "旺旺";

结果会报错,如下图

2-14.png

我们已经为属性,封装字段了,但是为什么没有办法给字段赋值呢?我们就来探究一下这个问题。

当我们实例化Dog对象,即

.NET Framwork做了什么工作呢?如下图:

2-15.png

那为什么会报错呢,原因如下图:

2-16.png

这次分享到这里就结束了。其实蛮享受写这个过程的。因为在初次的学的时候理解了,如果再写成博客就又加深了印象,最后希望大家都能养成了良好的学习习惯。

3

结构,GC回收,静态成员,静态类

目录:

[【C#小知识】C#中一些易混淆概念总结][1]

一,C#中结构

在C#中可以使用struct关键字来定义一个结构,级别与类是一致的,写在命名空间下面。

1)结构中可以定义属性,字段,方法和构造函数。示例代码如下:

//定义结构
struct Point
{

//定义字段
private int x;

//封装字段
public int X
{
get { return x; }
set { x = value; }
}

//定义方法
public void Result()
{

}

//定义构造函数
public Point(int n)

{
this.x = n;
//Console.WriteLine(n);
}

}

那么,声明类与结构的区别有哪些呢?

①无论如何,C#编译器都会为结构生成无参数的构造函数;

当我们显式的定义无参数的构造函数,编译时会报错,结果如下:

3.1.png

编译器告诉我们,结构不能包含显式的无参数的构造函数

但是这样编写代码时,编译器却不报错,代码如下:

//这里可以调用无参数的构造函数
Point p = new Point();
Console.WriteLine(p.GetType());

运行结果如下:

3.2.png

虽然结构不能显式的声明无参数的构造函数,但是程序员却可以显式的调用结构的无参数的构造函数,说明C#编译器无论如何都会为结构生成无参数的构造函数。

②结构中的字段不能赋初始值;

3.3.png

③在结构的构造函数中必须要对结构体的每一个字段赋值;

当我们不声明显式的构造函数时,可以不对成员字段赋值,但是一旦声明了构造函数,就要对所有的成员字段赋值

3.4.png

对所有的成员字段赋值,代码如下:

     //定义构造函数
public Point(int n)
{
this.x = n;
//Console.WriteLine(n);
}

④在构造函数中对属性赋值不认为对字段赋值,属性不一定去操作字段;

3.5.png

所以在构造函数中我们对字段赋初始值的时候,正确的代码应该是

//定义构造函数
public Point(int n)
{
//正确的可以对字段赋初始值
this.x = n;

//在构造函数中对属性赋值,但是不一定操作字段
this.X = n;
//Console.WriteLine(n);
}

2)结构体的数值类型问题

C#中的结构是值类型,它的对象和成员字段是分配在栈中的,如下图:

3.6.png

那么当我们写了如下的代码,内存中发生了什么呢?

     //这里可以调用无参数的构造函数
Point p = new Point();
//为p的属性赋值
p.X = 100;
//将p赋值给Point新的对象p1
Point p1 = p;

Point p1=p发生了什么呢?情况如下:

3.7.png

声明结构体对象可以不使用"new"关键字如果不使用"new"关键字声明结构体对象,因为没有调用构造函数,这个时候结构体对象是没有值的。而结构的构造函数必须为结构的所有字段赋值,所以通过"new"关键字创建结构体对象的时候,这个对象被构造函数初始化就有默认的初始值了。实例代码如下:

class Program
{
static void Main(string[] args)
{
//没有办法调用默认的构造函初始化
Point p;
Console.WriteLine(p);

//会调用默认的构造函数对的Point对象初始化
Point p1 = new Point();
Console.WriteLine(p1);
Console.ReadKey();

}
}
//定义结构
struct Point
{
//定义时赋初始值,编译器会报错
private int x;
}

编译的时候会报错:

3.8.png

3)结构体不能使用自动属性

在第一篇文章我写自动属性的时候,反编译源代码,知道自动属性,会生成一个默认字段。而在结构的构造函数中需要对每一个字段赋值,但是编译器不知道这个字段的名字。所以,没有办法使用自动属性。

那么什么时候定义类,什么时候定义结构体呢?

首先我们都知道的是,栈的访问速度相对于堆是比较快的。但是栈的空间相对于堆来说是比较小的。

①当我们要表示一个轻量级的对象,就可以定义结构体,提高访问速度。

②根据传值的影响来选择,当要传递的引用就定义类,当要传递的是"拷贝"就定义结构体。

二,关于GC(.NET的垃圾回收)

1)分配在栈中的空间变量,一旦出了该变量的作用域就会被CLR立即回收;如下代码:

//定义值类型的n当,程序出了main函数后n在栈中占用的空间就会被CLR立即回收
static void Main(string[] args)
{
int n = 5;
Console.WriteLine(n);
}

2)分配在堆里面的对象,当没有任何变量的引用时,这个对象就会被标记为垃圾对象,等待垃圾回收器的回收;

GC会定时清理堆空间中的垃圾对象,这个时间频率是程序员无法控制的,是由CLR决定的。所以,当一个对象被标记为垃圾对象的时候,不一定会被立即回收。

3)析构函数

在回收垃圾对象的时候,析构函数被GC自动调用。主要是执行一些清理善后工作。

析构函数没有访问修饰符,不能有你参数,使用"~"来修饰。 如下面的代码示例:

class Program
{
//定义值类型的n当,程序出了main函数后n在栈中占用的空间就会被CLR立即回收
static void Main(string[] args)
{
int n = 5;

OperateFile operate = new OperateFile();

operate.FileWrite();
//执行完写操作后,会调用该类的析构函数,释放对文件对象的控制
//Console.WriteLine(n);
}
}

//定义操作硬盘上文件上的类
class OperateFile
{
//定义写文件的方法
public void FileWrite()
{ }

//定义调用该类结束后,所要执行的动作
~OperateFile()
{
        //释放对操作文件对象的控制
}
}

** 三,静态成员和实例成员的区别:**

静态成员是需要通过static关键字来修饰的,而实例成员不用static关键字修饰。他们区别如下代码:

class Program
{
static void Main(string[] args)
{
//静态成员属于类,可以直接通过"类名.静态成员"的方式访问
Person.Run();

//实例成员属于对象,需要通过"对象名.实例成员"来访问
Person p = new Person();
p.Sing();
}
}

class Person
{
//静态成员变量
private static int nAge;
//实例成员变量
private string strName;

public static void Run()
{
Console.WriteLine("我会奔跑!");
}

public void Sing()
{
Console.WriteLine("我会唱歌");
}
}

当类第一次被加载的时候(就是该类第一次被加载到内存当中),该类下面的所有静态的成员都会被加载。实例成员有多少对象,就会创建多少对象。

而静态成员只被加载到静态存储区,只被创建一次,且直到程序退出时才会被释放。

看下面的代码:

class Program
{
static void Main(string[] args)
{

Person p = new Person();
Person p1 = new Person();
Person p2 = new Person();

}
}

class Person
{
//静态成员变量
private static int nAge;
//实例成员变量
private string strName;

public static void Run()
{
Console.WriteLine("我会奔跑!");
}

public void Sing()
{
Console.WriteLine("我会唱歌");
}
}

那么在内存中发生了什么呢?如下图:

3.9.png

由上面显然可知,定义静态的成员是可以影响程序的执行效率的。那么什么时候定义静态的成员变量呢?

①变量需要被共享的时候②方法需要被反复的调用的时候

2)在静态方法中不能直接调用实例成员。

当类第一次被加载的时候,静态成员已经被加载到静态存储区,此时类的对象还有可能能没有创建,所以静态方法中不能调用类成员字段。实例代码如下:

3.10.png

this和base关键字都不能在静态方法中使用。

②可以创建类的对象指明对象的成员在静态方法中操作,代码如下:

public static void Run()
{
Person p = new Person();
p.strName = "强子";
Console.WriteLine("我会奔跑!");
}

③在实例成员中肯定可以调用静态方法,因为这个时候静态成员肯定存在,代码如下:

public static void Run()
{
Person p = new Person();
p.strName = "强子";
Console.WriteLine("我会奔跑!");
}

public void Sing()
{
//实例方法被调用的时候,对象实例一定会被创建,所以可以在实例方法中访问实例的字段
this.strName = "子强";
strName = "子强";

//调用静态成员
Run();
Console.WriteLine("我会唱歌");
}

静态成员和实例成员的对比:

①生命周期不一样

静态成员只有在程序结束时才会释放,而实例成员没有对象引用时就会释放

②内存中存储的位置不一样

静态成员存放在静态存储区,实例成员在托管堆中。

四,静态类

①静态类被static关键字修饰

//定义两个静态类
static class Person
{ }

internal static class Cat
{ }

②静态类中只能生命静态的成员变量,否则会报错(因为访问该实例成员的时候,类的对象可能还没有被创建)

3.11.png

③静态类中不能有实例的构造函数(如果有实例的构造函数,则该静态类能被实例化,都是静态成员,没有实例成员被调用)

3.12.png

正确的声明方法:

static class Person
{
//private int nAge;
private static string strName;

static Person()
{
}
}

④静态类不能被继承,反编译刚才的两个类,结果如下:

3.13.png

会发现静态类的本质是一个抽象密封类,所以不能被继承和实例化。所以,静态类的构造函数,不能有访问修饰符

2)那么什么时候声明静态类呢?

如果这个类下面的所有成员的都需要被共享,可以把这个类声明为静态类。

且在一般对象中不能声明静态类型的变量(访问该静态变量时,可能该对象还没有被创建)。

3.14.png

3)静态类的构造函数

静态类可以有静态的构造函数(且所有类都可以有静态的构造函数),如下代码:

class Program
{
static void Main(string[] args)
{
Cat c;
Cat c1 = new Cat();

Console.ReadKey();
}
}

class Cat
{
private int n;
public string strName;

//实例构造函数
public Cat()
{
Console.WriteLine("看谁先执行2");
}

//静态构造函数
static Cat()
{
Console.WriteLine("看谁先执行1");
}

}

执行结果如下:

3.15.png

由此我们可以知道,静态的构造函数会先于实例构造函数执行

//不执行静态构造函数
Cat c;

当我们在Main()函数中添加如下的代码是:

static void Main(string[] args)
{
//不执行静态构造函数
Cat c;
Cat c1 = new Cat();
Cat c2 = new Cat();

Console.ReadKey();
}

运行结果如下:

3.16.png

说明静态的构造函数只执行了一次。

好吧这次的分享风到此结束。希望对大家对理解C#基础概念知识能有所帮助。

4

解析Console.WriteLine()

这几天在温习结构体和类的时候遇到一个问题。发现一个奇怪的现象,一直找不到合理的答案。但是今天终于找到了合理的答案,所以拿来和大家分享一下。

class Program
{
static void Main(string[] args)
{

Point p;
Console.WriteLine(p);

Point p1 = new Point();
Console.WriteLine(p1);
Console.ReadKey();

}
}
//定义结构
struct Point
{
////定义时赋初始值,编译器会报错
//private int x;

//public Point()
//{ }
}

class Person
{
//在类中我们可以为属性赋初始值
//private int nAge = 5;

//public int NAge
//{
// get { return nAge; }
// set { nAge = value; }
//}

}

当我们只是声明一个类和一个结构体的时候,我们的编译器顺利的编译通过。并且打印出结果如下:

4.1.png

为什么我们没有在结构和类中做任何操作,却可以打印出结果,且是"命名空间+"."+数据类型"呢?

首先我查阅了MSDN的关于结构(struct)的官方文档(地址点击这里),有如下的一段话:

4.10.png

结构默认的构造函数(如果没有显式声明)在实例化的时候才会被调用。所以,

       //结构的实例化可以不使用NEW关键字,只是将p加载到栈空间中,但是对象不可用,这里没有调用默认的构造函数
Point p2;
Console.WriteLine(p2);

Console.WriteLine(p2);
Console.ReadKey();

在内存中是如下的情况:

4.2.png

此时在栈中已经存在了p这个对象,但是不可用。

那么为什么会打印出"命名空间+"."+数据类型"的结果呢?

我们先看一下VS编译后的中间代码,即Msil,详细解释在图中给出:

4.3.png

有中间语言代码,我们可以知道,最后调用的是Console.WriteLine(Object)方法

这时候就要深入的研究一下Console类了,用反编译工具.NET Reflector查看Console类,因为在上面的代码中,传进.WriteLine()方法的是一个类,所以,我们要查看它的的(object value)方法,如下图:

4.4.png

这时候,我们再深入到WriteLine()方法中去,源代码,如下:

4.5.png

再看Out.WriteLine()的源代码:

4.6.png

因为p已经在栈中创建了对象(但是不可用),所以,直接进入else语句。

明显的可以发现IFormattable是一个接口,我们再看IFormattable接口的源码,如下:

4.7.png

显然我们的Point 结构没有实现一个ToString()方法,不存在继承关系,所以会转化失败,返回一个null值,又进入下一个else语句

else
{
this.WriteLine(value.ToString());
}

这时候最重要的就要来了,我们看到value值被转换为字符串输出了,在看ToString()源代码,如下:

4.8.png

很明显的发现,是获取该对象的数据类型并且转化为字符串输出。如下代码:

        Point p;
//打印出p的数据类型
Console.WriteLine(p.GetType());

Point p2;
Console.WriteLine(p2);

//使用NEW实例化了对像,调用了默认的构造函数
Point p1 = new Point();
Console.WriteLine(p1);
Console.ReadKey();

打印结果:

4.9.png

这样对结构和类的了解有没有更深入的了解呢?

5

继承

这次主要分享的内容是关于继承的知识。

首先,我们先来看看继承;

既然有继承,就要有父类和子类,来看下面的一段代码:

class Person
{
private int nAge;
protected string strName;
double douHeight;
public string strEateType;

public void Hello()
{
Console.WriteLine("我可以说Hello!");
}
public void Run()
{
Console.WriteLine("我可以跑!");
}
}

class Student : Person
{

}

然后我在Main()函数中实例化子类的对象,代码如下:

static void Main(string[] args)
{
Student stu1 = new Student();
}

那么在这个过程中内存中发生了些什么呢?

我们先来看misl的中间代码,看看那能发现些什么

5.1.png

由此我们可以发现子类继承了父类的所有成员包括Private和Protect,并为这些成员开辟了空间来存储。

我们再来实例化我们的子类,然后访问父类的字段和方法,会发现,如下的现象

5.2.png

所以虽然子类为父类的所有成员在堆中都开辟了空间,但是父类的私有成员(Private)子类访问不到,

而受保护的成员(protected)可以通过实例化对象访问的到。

所以在内存中的情况如下图:

5.3.png

看下面的代码,我们来探究一下在子类中this关键字和base关键字所访问的类的成员有哪些,代码如下:

class Student : Person
{
private string strClass;

private string strAddress;

public void Address(string cla, string adre)
{
//这里的this关键字调用了子类的成员和父类的非似有成员
this.strClass = "五";
this.strAddress = "北京";
this.strName = "子强";

//这里base关键字调用了是父类的非似有成员
base.strName = "强子";

Console.WriteLine("我是{0}年纪,来自{1}", cla, adre);
}
public void Sing()
{
this.strClass = "";
Console.WriteLine("我可以唱歌!");
}
}

所以在子类中this关键字和base关键字的访问范围的示意图如下:

5.4.png

二,关于子类对象的构造函数和父类构造函数的执行顺序

我们分别为父类和子类添加显式的构造函数,代码如下

class Person
{
private int nAge;
protected string strName;
double douHeight;
public string strEateType;

//父类的构造函数
public Person()
{
Console.WriteLine("我是父类的构造函数");
}

public void Hello()
{
Console.WriteLine("我可以说Hello!");
}
public void Run()
{
Console.WriteLine("我可以跑!");
}

}

class Student : Person
{
private string strClass;

private string strAddress;

//子类的构造函数
public Student ()
{
Console.WriteLine("我是子类的构造函数");
}
}

我们使用VS的单步调试,来看父类和子类显式构造函数的执行顺序,如下图(动态图片,可以看到过程):

5.5.gif

很容易的可以发现,当创建子类对象的时候

①先调用了子类的构造函数

②调用了父类的构造函数

③执行了父类的构造函数

④执行了子类的构造函数

那么为什么会这样呢?

我尝试通过反编译看源码来解释这个原因,但是反编译的结果如下,

5.6.png

没有发现有什么特别的地方可以解释这个原因。

最后还是查阅微软的MSDN官方文档找到了答案(原文地址点击这里

5.7.png

根据微软官方的代码示例,那么下面的代码的效果也是相同的

//子类的构造函数
public Student ()
{
Console.WriteLine("我是子类的构造函数");

}

//这里的代码和上面的代码效果是相同的
public Student()
:base()
{
Console.WriteLine("我是子类的构造函数");
}

也就是说只要在子类显式的声明了无参的构造函数,在实例化子类的对象是,子类的无参构造函数都会去调用父类无参的构造函数。

那么,如果父类没有这个无参的构造函数则会报错。

如下面的代码:

class Person
{
private int nAge;
protected string strName;
double douHeight;
public string strEateType;

//父类的构造函数
//public Person()
//{
// Console.WriteLine("我是父类的构造函数");
//}

      //父类的有参数的构造函数,这里覆盖了无参的构造函数
public Person (string str)
{
Console.WriteLine("我是父类的构造函数{0}",str);
}

public void Hello()
{
Console.WriteLine("我可以说Hello!");
}
public void Run()
{
Console.WriteLine("我可以跑!");
}
}

class Student : Person
{
private string strClass;

private string strAddress;

//子类的无参构造函数
public Student ()
{
Console.WriteLine("我是子类的构造函数");

}

public Student(string strName)
{
Console.WriteLine("我的名字叫{0}",strName);
}
}

这时候编译会报错,

5.8.png

因为在父类中有参数的构造函数覆盖了无参数的构造函数,所以在子类的无参数的构造函数没办法回调父类的无参数的构造函数初始化父类的成员变量。所以报错。

那么在初始化子类的时候,为什么要调用父类的构造函数呢?

在初始化子类之前需要通过构造函数初始化父类的成员变量

父类的构造函数先于子类的构造函数执行的意义是什么呢?

当在父类的构造函数中和子类的构造函数中为父类的非私有成员变量赋不同默认值。当实例化子类,子类要调用构造函数初始化成员变量,如果先执行了子类的构造函数,再执行父类的构造函数,父类成员字段的值会覆盖子类成员字段的值。但是我们想得到的是子类的属性值。所以为了解决数据冲突,父类的构造函数要先于子类的构造函数执行。

如下面的代码:

class Person
{
private int nAge;
private string strName;
double douHeight;
public string strEateType;

// 父类的构造函数
public Person()
{
//再父类中对strEateType赋初始值
this.strEateType = "吃饭";
Console.WriteLine("我是父类的构造函数{0}", strEateType);
}
}

class Student : Person
{
private string strClass;
private string strAddress;

//子类的构造函数
public Student()
{
//在子类中对strEateType赋初始值
this.strEateType = "吃面条";
Console.WriteLine("我是子类的构造函数{0}",strEateType);

}
}

这时候我们通过,声明子类对象访问strEateType的值,如下:

Student stu1 = new Student();
//stu1.
string str = stu1.strEateType.ToString();
Console.WriteLine(str);

Console.ReadKey();

这里肯定是要打印出子类的属性strEateType的值,如果先执行子类构造函数对strEateType赋值,然后父类的构造函数赋值覆盖strEateType的初始值。那么打印出的将是父类成员字段的值。所以,父类的构造函数先于子类的构造函数执行。

打印结果如下:

5.9.png

三,子类是否可以有和父类的同名方法

看下面的代码,我们声明一个父类Person:**
**

class Person
{
private int nAge;
private string strName;
double douHeight;
public string strEateType;

public readonly string strrrr;
// 父类的构造函数
public Person()
{
this.strEateType = "吃饭";
Console.WriteLine("我是父类的构造函数{0}", strEateType);
}

public Person(string str)
{
this.strName = str;
Console.WriteLine("我是父类的构造函数{0}", str);
}

public void Hello()
{
Console.WriteLine("我可以说地球人的Hello!");
}
public void Run()
{
Console.WriteLine("我可以跑!");
}
}

声明一个子类继承Person,代码如下:

class Worker:Person
{
public void Hello()
{
Console.WriteLine("我是工人会说Hello!");
}

public new void Run()
{
Console.WriteLine("我是工人我会奔跑!");
}
}

然后实例化Worker对象,打印Hello方法,结果如下图:

5.10.png

这是为什么呢?编译器已经告诉了我们,如下图:

5.11.png

**看出来是子类的方法隐藏了父类的方法。

既然子类可以定义和父类同名的方法,那么是否可以定同名的字段呢?答案是肯定的,而且会像同名方法一样,子类同名字段会隐藏父类同名的字段。

6

解析里氏替换原则,虚方法

这一系列的文章在园子里还是比较受欢迎的。有一些留言指出了其中理论性的错误,还有问自己是否毕业,怎么写出来这些文章的,有没有培训过等等问题。

下面就一并的回答这些问题吧。
1)自己今年六月份毕业,现在在帝都实习。不过在学校已经做过一些C#开发了,现在也是做.NET开发工作。

2)文章中很多知识是自己以前在网上下载的视频教程,学习过程中所记的笔记。也就是在年前的时候,突然有一天发现自己的笔记本记了差不多块一本了,之前也没时间整理过,所以就想着把它们整理成博客文章,顺便温习一下这些笔记知识。

3)有园友问自己是不是在传智培训过。首先说我没有培训过,但是非常感谢传智公开的一些自学教程。因为自己也是这些视频的受益者,学到了很多知识,养成了一些好的学习习惯。

4)在整理笔记的过程中遇到了很多问题,其中自己参考了《C#本质论》,《CLR via C#》还有就是MSDN的官方文档。

3)不管怎样还是会遇到一些自己解决不掉或者弄不清楚的问题,这个过程使用了Google搜索并和请教了一些园友。

4)错误总是会存在。谢谢看我博客的读者你们的细心,指出了我博文中的错误。确定这些错误后,我都立即修改了自己的文章。

今天开始上班了。这几天研究学习了一下思维导图,感觉用它整理自己的知识非常的方便。所以,以后写博客完成一个知识点,都会用思维导图做一个总结。也能让大家对所要读的内容有一个整体的把握。

我用的思维导图软件是FreeMind(免费的,但是得装JDK),因为刚开始学习使用,很多操作技巧不是很熟练,做出来的导图估计也不是很好,希望大家见谅。

首先,里氏替换原则。

这是理解多态所必须掌握的内容。对于里氏替换原则维基百科给出的定义如下:

6.1.png

为什么子类可以替换父类的位置,而程序的功能不受影响呢?

当满足继承的时候,父类肯定存在非私有成员,子类肯定是得到了父类的这些非私有成员(****假设,父类的的成员全部是私有的,那么子类没办法从父类继承任何成员,也就不存在继承的概念了)。既然子类继承了父类的这些非私有成员,那么父类对象也就可以在子类对象中调用这些非私有成员。所以,子类对象可以替换父类对象的位置。

来看下面的一段代码:

class Program
{
static void Main(string[] args)
{
Person p = new Person();

Person p1 = new Student();

Console.ReadKey();
}
}

class Person
{
    //父类的私有成员
    private int nAge;

public Person()
{
Console.WriteLine("我是Person构造函数,我是一个人!");
}

public void Say()
{
Console.WriteLine("我是一个人!");
}

}

class Student : Person
{
public Student()
{
Console.WriteLine("我是Student构造函数,我是一个学生!");
}

public void SayStude()
{
Console.WriteLine("我是一个学生!");
}
}

class SeniorStudent : Student
{
public SeniorStudent()
{
Console.WriteLine("我是SeniorStudent构造函数,我是一个高中生!");
}
public void SaySenior()
{
Console.WriteLine("我是一个高中生!");
}
}

我们运行打印出的结果是:

6.2.png

根据前面的构造函数的知识很容易解释这个结果。那么我们在Main()函数中添加如下的代码:

static void Main(string[] args)
{
Person p = new Person();
p.Say();
  
Person p1 = new Student();
p1.Say();
Console.ReadKey();
}

在访问的过程中,可以发现p只可以访问父类的say

6.3.png

而p1也只可以访问父类的Say方法

6.4.png

其实在上面的代码中,就满足了里氏替换原则。子类的Student对象,替换了父类Person对象的位置。

那么它们在内存中发生了些什么呢?如下图:

6.5.png

由上可以知道,当一个父类的变量指向一个子类对象的时候只能通过这个父类变量调用父类成员,子类独有的成员无法调用。

同理我们可以推理出,子类的变量是不可以指向一个父类的对像的

6.6.png

但是当父类变量指向一个子类变量的时候,可以不可以把父类的变量转化成子类的对象呢?看下图

6.7.png

关于引用类型的两种转换方式:

由上面的代码我们已经知道了一种转换,就是在变量钱直接加需要转换的类型,如下代码:

Student s2 = (Student)p1;

那么第二种转换方式就是使用as关键字,如下代码:

//将指向子类对象的变量转化成子类类型
Student s2 = (Student)p1;

//使用as关键字,转换失败返回一个null值
Student s3 = p1 as Student;

使用as关键字和第一种强制转换的区别就是,第一种如果转换失败会抛异常,第二种转换失败则返回一个null值。

思维导图总结如下:

6.8.png

** 二,虚方法**

使用virtual关键字修饰的方法,叫做虚方法(一般都是在父类中)。

看下面的一段代码:

class Person
{
private int nAge;
public Person()
{
Console.WriteLine("我是Person构造函数,我是一个人!");
}

//这里定义了一个虚方法
public virtual void Say()
{
Console.WriteLine("我是一个人!");
}

}

class Student : Person
{
//子类使用override关键字改写了父类的虚方法
public override void Say()
{
Console.WriteLine("我是一个学生!");
}
public Student()
{
Console.WriteLine("我是Student构造函数,我是一个学生!");
}

public void SayStude()
{
Console.WriteLine("我是一个学生!");
}
}

紧接着在main()函数中添加如下的代码:

static void Main(string[] args)
{
Person p = new Person();
p.Say();

Person p1 = new Student();
p1.Say();

Student s = new Student();
s.Say();
Console.ReadKey();
}

打印结果如下:

6.9.png

我们很明显的可以发现,第二个表达式满足里氏替换原则,p1.Say()执行的应该是父类的Say()方法,但是这里却执行了子类的Say()方法。

这就是子类使用override关键字的Say()方法覆盖了父类的用Virtual关键字修饰的Say()方法。

我们使用动态图片看一下调试过程,

①首先是没有使用任何关键字:

6.10.gif

由上可以看出直接跳入父类,执行了父类的Say()方法;

②再看使用virtual和override关键字的动态调试图片,如下:

6.11.gif

可以看到直接到子类去执行override关键字修饰的Say()方法。

那么如果父类使用virtual关键字修饰,而子类没有重写该方法时会怎么样呢?如下面的代码:

class Program
{
static void Main(string[] args)
{

Person p1 = new Student();
p1.Say();
Console.ReadKey();
}
}

class Person
{
private int nAge;
public Person()
{
Console.WriteLine("我是Person构造函数,我是一个人!");
}

//这里定义了一个虚方法
public virtual void Say()
{
Console.WriteLine("我是一个人!");
}

}

class Student : Person
{
//子类中没有出现override关键字修饰的方法

public void SayStude()
{
Console.WriteLine("我是一个学生!");
}
}

执行结果如下:

6.12.png

所以,如果子类找不到override方法,则会回溯到该子类的父类去找是否有override方法,知道回溯到自身的虚方法,并执行。

虚方法知识总结的思维导图如下:

6.13.png

7

解析抽象类,抽象方法

我笔记本上C#方面的知识基本上整理的差不多了,所以这个关于《C#小知识》的系列估计就要完结了,这个过程中谢谢大家一直来对于我的支持,你们给我的宝贵的意见对我帮助很大。

在介绍抽象类和抽象方法之前还是先提一下多态的基本概念。

其实在上一篇关于里氏替换原则就已经说明了多态的精髓"子类对象可以替换父类对象的位置,而程序的功能不受影响"。还是来看一段代码吧:

///

/// Create By:ZhiQiang
/// Create Time:2014-2-9
///

class Person
{

//定义虚方法以备子类重写,当子类替换父类对象的位置时,可以表现出多态
public virtual void Run()
{
Console.WriteLine("我是人,我会跑!");
}

public virtual void Say()
{
Console.WriteLine("我是人,我会说话!");
}
}

子类的代码如下:

//定义Teacher类继承Person
class Teacher:Person
{
public override void Run()
{
Console.WriteLine("我是老师,我必须慢速跑");
}

public override void Say()
{
Console.WriteLine("我是老师,我得说表扬的话!");
}

}

//定义Student类继承Person
class Student : Person
{
//子类重写了父类的虚方法
public override void Run()
{
Console.WriteLine("我是学生,我会加速跑!");
}

public override void Say()
{
Console.WriteLine("我是学生,我会说英语!");
}
}

下面需要一个实现多态的类,代码如下:

//实现多态的类
class FeatureHuman
{
///

/// 这个方法就提现了多态,当传入的是子类的对象的时候,p指向的是子类对象,就可以调用子类重写父类方法后的方法
///

/// 父类或者子类对象
public void OutPutFeature(Person p)
{
p.Run();
p.Say();
}
}

主体代码和实现多态的方法如下:

static void Main(string[] args)
{
FeatureHuman fea = new FeatureHuman();
//人的特点
Person p = new Person();

Program pro = new Program();
fea.OutPutFeature(p);

//学生的特点
Student s = new Student();
fea.OutPutFeature(s);
//老师的特点
Teacher t = new Teacher();
fea.OutPutFeature(t);

Console.ReadKey();
}

运行,打印结果如下:

7.1.png

这里可以发现,我们outputFeature方法根据传入的实体对象不同(父类变量指向了子类的对象),而打印出了不同人物的特点,这就是多态。

代码图解如下:

7.2.png

多态总结如下:

7.3.png

二,抽象类和抽象方法

在C#中使用abstract关键字修饰的类和方法,叫做抽象类和抽象方法。

1)抽象类中可以拥有没抽象成员,为了继承给他的子类调用 (抽象类就是为了定义抽象成员,继承给子类去实现,同时子类也可以调用父类的非抽象成员)

abstract class Person
{
//private int nAge;

//abstract string strName;

//抽象类可以包含不抽象的成员,可以给继承的子类使用
public void Say()
{
Console.WriteLine("我是父类,我是人!");
}

public virtual void Sing()
{
Console.WriteLine("我是父类,我是人,我可以唱歌!");
}

//Run的抽象方法
public abstract void Run();
}

2)抽象类中可以有virtual修饰的虚方法

如上面的代码,在抽象类中定义了virtual修饰的方法,编译通过。抽象类就是为了定义抽象成员,继承给子类去实现,所以子类也可以实现抽象类中的虚方法。

3)抽象类不能实例化,因为有抽象成员,而抽象成员没有方法体,如下图,

7.4.png

**4)抽象成员不能私有,如果私有子类没有办法访问 **

我们可以在抽象类中定义私有成员,但是没有意义。因为子类根本访问不到这些私有成员,而抽象类本身也不能实例化,所以私有成员访问不到。

**5)子类必须重写父类的抽象方法 **

在上面代码的基础上,我们定义一个Student类,继承抽象类,但是不实现抽象类的抽象方法,编译报错。代码如下:

7.5.png

**6)在子类中没有办法通过base关键字调用父类抽象方法 **

原理同上,抽象类的抽象发放没有实现语句,就算调用也没有意义。但是可以使用base关键字调用非抽象方法,代码如下:

class Program
{
static void Main(string[] args)
{
//Person p = new Person();
Student s = new Student();
s.Run();
Console.ReadLine();
}
}

class Student : Person
{

public override void Run()
{
base.Say();
Console.WriteLine("我是学生,继承了父类,我可以跑!");
}
}

打印结果如下:

7.6.png

抽象类思维导图总结如下:

7.7.png

抽象方法

1)抽象方法必须定义在抽象类中,

class Student : Person
{
public abstract void Swiming();

public override void Run()
{
base.Say();
Console.WriteLine("我是学生,继承了父类,我可以跑!");
}
}

代码编译会报错,如下图:

7.8.png

2)抽象方法必须使用关键字修饰,示例代码如下:

abstract class Person
{
//private int nAge;

//abstract string strName;

//抽象类可以包含不抽象的成员,可以给继承的子类使用
public void Say()
{
Console.WriteLine("我是父类,我是人!");
}

public virtual void Sing()
{
Console.WriteLine("我是父类,我是人,我可以唱歌!");
}

//Run的抽象方法,不能有方法体,留给子类实现
public abstract void Run();

抽象方法思维导图总结如下:

7.9.png

** 那么什么时候使用抽象类呢?**

①子类必须重写父类的方法(相当于定义了一个标准,规范)

②父类没有必要实例化,就用抽象类

③抽象类是为了继承,为了多态

最后来看一个示例代码:

定义一个抽象类,其中包含抽象方法Run()

abstract class Person
{//Run的抽象方法,只要是继承我的子类都要实现这个方法
public abstract void Run();
}

分别定义两个子类,继承抽象类Person

class Student : Person
{
//public abstract void Swiming();

public override void Run()
{
// base.Say();
Console.WriteLine("我是学生,继承了父类,我可以跑!");
}
}

class Worker:Person
{

public override void Run()
{
Console.WriteLine("我是工人,继承了父类,我每天在厂区跑!");
}
}

为了表现多态,我们编写一个方法如下:

//该方法变现了多态,根据需要返回子类的对象
public static Person GetEntity(string str)
{
if(str=="学生")
{
return new Student();
}
else if(str=="工人")
{
return new Worker();
}
return null;
}

main函数中的代码如下:

static void Main(string[] args)
{
//不直接实例化父类对象,只是声明一个父类对象的变量来接收方法的返回值
Person p = GetEntity(Console.ReadLine());
p.Run();

Console.ReadLine();
}

运行,分别输入"工人"和"学生"的打印结果如下:

7.10.png

到这里这一部分的内容就结束了,希望大家多多提宝贵的意见。

8

解析接口

这一篇主要来解析关于面向对象中最总要的一个概念——接口。

对于接口来说,C#是有规定使用Interface关键字来声明接口。它的声明是和类一致的。可以说接口就是一个特殊的抽象类。如下代码:

class Program
{
static void Main(string[] args)
{
}
}

//声明一个可以飞的接口
interface IRunable
{
//包含可以被继承的子类实现的方法
void Run();
}

由以前的抽象类的知识可以知道,抽象类是没有办法实例化的(因为含有抽象成员,而抽象成员不含有方法体)。那么接口可不可以实例化呢?答案是肯定的,不能实例化。看下面的一段代码:

8.1.png

这个时候编译器告诉我们无法创建抽象类或者接口的实例。

二,接口可以定义哪些成员

1)接口就是一个定义"具有某种能力的抽象类",既然接口是类,那么它的内部可以定义哪些成员呢?

首先,在普通的类中,可以有字段,属性,方法,索引器,抽象方法等等。那么接口呢?

看下面直接声明字段,编译器会报错,告诉我们接口内不能声明字段

8.2.png

既然接口内不能有字段,那也就不存在封装字段了。所以上边图示的封装字段的代码也是错误的。

同理由上面的代码也可以知道,在接口中是不可以定义显式的属性(因为在属性中要操作字段赋值,但是字段没有办法在接口中声明)。

那么接口可以声明自动属性么?看下面的代码:

//声明一个可以飞的接口
interface IRunable
{
//声明字段
int nAge { get; set; }

string strName { get; set; }
////包含可以被继承的子类实现的方法
void Run();

}

代码可以顺利编译通过,那么是为什么呢?这就要看.NET的源码,我把源码编译后的比较结果如下图:

8.3.png

抽象方法就不用多了,本来接口就是一个抽象爱类,当然可以定义抽象类,但是不在使用abstract关键字,而且方法必须没有方法体;

2)继承接口的子类必须实现接口的所有抽象成员。

我们先来看下面的代码:

//声明一个接口,其中包含属性和未实现方法void
interface IRunable
{
string strName { get; set; }
void Run();
}

下面来一个实现类,如下:

class Person:IRunable
{
public void Run()
{
Console.WriteLine("我可以奔跑!");
}
}

这时候,我们编译,编译器会告诉我们什么呢?如下图:

8.4.png

所以继承接口的类,必须实现接口的所有抽象成员。

正确的代码如下:

class Person:IRunable
{
public void Run()
{
Console.WriteLine("我可以奔跑!");
}

public string strName
{
get
{
return strName;
}
set
{
strName = value;
}
}
}

通过以上的代码可以发现:

①我们的继承类在实现接口成员的时候不需要使用override关键字

②实现接口的时候,必须保持签名一致

由前面抽象类的知识我们有没有这样的疑问,什么时候使用抽象类,什么时候使用接口呢?

总结如下:

①使用抽象类:可以找到父类,并且希望通过父类继承给子类一些成员

②使用接口:接口就是一个纯粹的为了规范实现的类。比如:多个类具有相同的方法,但是却找不到父类,就可以将方法定义在接口中。让这些类去实现。

下面纠纷别来看两端代码,比较抽象类和接口的异同,首先是抽象类:

class Program
{
static void Main(string[] args)
{
Student s = new Student();
//Student类通过继承获得NAge属性
s.NAge = 10;
s.Eat();

Console.WriteLine("--------Student和Worker类分别通过继承获得了父类的非私有成员,实现了父类的抽象方法--------");

Worker w = new Worker();
//Worker类通过继承获得NAge属性
w.NAge = 40;
w.Eat();

Console.ReadKey();
}
}

//定义父类
abstract class Person
{
private int nAge;

public int NAge
{
get { return nAge; }
set { nAge = value; }
}

private void Run()
{
Console.WriteLine("我是父类,我可以跑!");
}
public abstract void Eat();

}

class Student : Person
{
//子类覆写了父类的抽象方法
public override void Eat()
{
Console.WriteLine("我是子类,我继承了父类,我可以在学校吃饭!");
}
}

class Worker:Person
{
//同样Worker也通过继承获得了父类的非私有成员
public override void Eat()
{
Console.WriteLine("我是子类,我继承父类,我可以在工厂吃饭");
}
}

接下来,来看看接口是怎么规范多个类的实现的。

class Program
{
static void Main(string[] args)
{
Student s = new Student();
s.strName = "小学生";
s.Run();
Console.WriteLine(s.strName);
Console.WriteLine("--------------------");
Worker w = new Worker();
w.strName = "看我能不能渎职";
w.Run();
Console.WriteLine(w.strName);

Console.ReadKey();
}
}

interface IRunable
{
//规范子类必须实现strName属性
string strName { get; set; }
//规范子类必须实现Run()方法
void Run();

}

class Student:IRunable
{
//这里是子类的字段
string strname;
public string strName
{
get
{
return strname;
}
set
{
strname = value;
}
}

public void Run()
{
Console.WriteLine("我是小学生,我在学校里面跑步!");
}

}

class Worker:IRunable
{
string strname;
public string strName
{
get
{
return "工人";
}
set
{
strname = value;
}
}

public void Run()
{
Console.WriteLine( "我是工人,我需要在厂区跑!");
}
}

由以上的代码可不可以发现,接口仅仅在规定一个规范子类的实现,而抽象类可以通过继承,继承给子类某些成员。

最后来看一下,接口的显示实现,我先看接口的普通实现(以上的代码实现接口的方式都是隐式实现)

interface IRunable
{
//规范子类必须实现strName属性
string strName { get; set; }
//规范子类必须实现Run()方法
void Run();

}

class Student:IRunable
{
//这里是子类的字段
string strname;
public string strName
{
get
{
return strname;
}
set
{
strname = value;
}
}

public void Run()
{
Console.WriteLine("我是小学生,我在学校里面跑步!");
}

}

显式实现接口

class Student:IRunable
{
//这里是子类的字段
string strname;
//显示实现接口
string IRunable.strName
{
get
{
return strname;
}
set
{
strname = value;
}
}

void IRunable.Run()
{
Console.WriteLine("我是小学生,我在学校里面跑步!");
}

}

显示的实现接口是为了解决方法名冲突的问题。但是显示实现接口会出现,在上面的代码中会出现一个问题,如下图:

8.5.png

为什么会这样呢?

因为显式实现接口的方法是私有的,不能通过对象变量来调用。那应该怎么调用呢,看下面的代码:

class Program
{
static void Main(string[] args)
{

//里氏替换原则,父类变量指向子类对象,并通过父类变量调用子类方法
IRunable ir = new Student();
ir.Run();
Console.ReadKey();
}
}

interface IRunable
{
//规范子类必须实现strName属性
string strName { get; set; }
//规范子类必须实现Run()方法
void Run();

}

class Student:IRunable
{
//这里是子类的字段
string strname;
//显示实现接口
string IRunable.strName
{
get
{
return strname;
}
set
{
strname = value;
}
}

void IRunable.Run()
{
Console.WriteLine("我是小学生,我在学校里面跑步!");
}

// Student s = new Student();

}

打印结果如下:

8.6.png

显式实现接口,这个接口的方法,只能通过接口变量来调用。

接口导图总结如下:

8.7.png

jk_book.png

jk_weixin.png

更多信息请访问 book_view.png

http://wiki.jikexueyuan.com/project/csharp-confusing-concepts-summary/

「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论