前言是我写完本篇文章后补充的:
如果道友是奔着Unity来学习的,只需要学到继承多态这里就可以了。后面完全是看道友有没有兴趣继续学下去。
该语言是一门面向对象语言,十分强大的语言,在微软平台上面发挥空间十分大,还有一些强大的功能,比如使用微软的winform平台,能直接弄一个电脑窗口,在winform上面写一个简易版的QQ更不是问题,但这些仍需慢慢探索。
本散仙是奔着去了解一下的心态,没想到学到后面越学越上瘾,因为C#这门语言能干的事情非常多,有些能够较快的得到反馈。
话不多说了,祝各位道友一路顺利~
用new开辟出来的数组都是动态数组,都是在堆中的内存。
我对于第一种的理解:赋值号的右边字面解释是->在堆中new10个空间出来,如果不加中括号赋值,初始化全部为0。如果要加中括号进行手动赋值的话,必须要赋值够10个,也就是说10个都要进行手动赋值,一个都不能少,否则就报错。(因为系统会自动给你赋初值为0 ,如果你手动赋值了,那么那些0都会丢掉,只能10个空间都由你来给,这就是为什么想要手动赋值就必须全部都赋上)。
这种方式与第一种一样的,也是在堆中new一个空间出来,但是这个空间取决于你在右边给出的出初始化好了的值,给出多少个就给你new多少个空间出来存放这些。
这种方式开辟的空间就是在栈上开辟出来的,因为是静态数组,所以是存放在栈中。
方法中传参必须在参数最右边作为参数
例如:方法名(参数1,参数2,params 参数3,参数4…);
意思就是这样,一旦使用params必须在最右边放参数,且这些参数的类型是一样的,同一个类型才能可以使用 params 关键字,就是说参数3,参数4往后的这些参数都必须是同一个类型的才行。
1.对于方法内,可以理解为就是个普通数组;
2.对于方法外部来说,参数可以为数组,也可以传一组相同的变量集合。
实例成员:用类命名的一个对象称为实例成员。
在同一个类中
下面是当我们用new创建一个对象时进行初始化的过程:
如何写构造函数:
public class Student
{
public Student(string name, int age, char sex)//主构造函数
{
this.Name = name;
this.Sage = age;
this.Ssex = sex;
}
public Student(string name) : this(name, 18, '女')
{
}
public Student(int age) : this( "空", age, '女')
{
}
public Student(char sex) : this("空", 18, sex)
{
}
string _sname;
public string Name
{
get { return _sname; }
set { _sname = value; }
}
int _sage;
public int Sage
{
get { return _sage; }
set {
if (value < 0 || value > 120)
value = 18;
_sage = value; }
}
char _ssex;
public char Ssex
{
get {
if (_ssex != '女' && _ssex != '男') return _ssex = '女';
return _ssex; }
set { _ssex = value; }
}
public void Sayhello()
{
Console.WriteLine("我叫{0}, 今年{1}岁, 性别{2}", this.Name, this.Sage, this.Ssex);
}
两种类型的存储方式:
注意:string[]和char[]不一样,string[]是字符串数组,都是指针指向堆里面存放的字符串类型,char[]就是每一个元素都是字符类型。
使用.Split这个函数的返回值是字符串数组,因为分隔后把各个部分做成字符串形式,然后进行返回。
并且注意:这里的分割是按照给出的字符进行分割几部分,不管你每个部分有多少字符或者一长串字符,都只给你按照指定的字符进行分割成几份,比如下面的就分割成三份,2022有四个字符,但是只给你看成一个东西进行分割。
public class Prgoram
{
public static void Main()
{
//将{2022—07—10}转化为2022年7月10日
string s = "2022—07—10";
string[] date = s.Split(new char[] {'—'}, StringSplitOptions.RemoveEmptyEntries);
//将字符串按照遇到—的形式进行分隔,每一个部分分割成字符串类型传到字符串数组中,并且移除掉—。
Console.WriteLine("{0}年{1}月{2}日",date[0],date[1],date[2]);
//Console.WriteLine(date[3]);
Console.ReadKey();
}
}
string ch = "日本安倍三阵亡";
if(ch.Contains("安倍三"))
{
ch = ch.Replace("安倍三", "***");
}
Console.WriteLine(ch);
Console.ReadKey();
1和2中的函数方法有重载。
方法4 , 5函数有重载
//使用分隔符把元素分隔开
string[] name = { "张三", "李四", "梨花", "荷花" };
string new_ch1 = string.Join("|", name);
Console.WriteLine(new_ch1);
string new_ch2 = string.Join("_", 1,2,5,4,9,6,7);
Console.WriteLine(new_ch2);
运行结果:

一个合法的字段和该字段属性代码如下:
private string _name; //私有字段,只允许在类的内部访问
public string Name //属性
{
get { return _name;}
set { _name = value; }
}
单根性的解释:每一个类都只能对应一个基类,对应方法是在派生类名右边加上 :基类名。
传递性的解释:就像一个族谱,父亲有他的父亲,我们叫父亲的父亲为爷爷,那么爷爷具有的一些品质会传到我们父亲身上,父亲身上的品质也会传到我们身上,又因为父亲有着爷爷的一些品质,那我们也就相当于有了爷爷的一些品质,所以这叫继承的传递性。
我的念头通达瞬间 ↓ :
构造函数的重载与类的构造函数继承问题有类似的地方,总结一下:
都是在构造函数中加上 : 冒号。
构造函数重载:在一个类中含有多个构造函数,目的是为了再初始化的时候能够按照需求初始化不同的值,但是也要一个全参的构造函数作为主体,其他重载构造函数就用 :this (主构造函数参数),**this的意思其实就是在一个类中进行重载构造函数。**如果次构造函数参数缺少主构造函数参数的话依旧按照主构造函数参数个数,强制赋上一个初值。总之一定要给出主构造函数的全参。
类之间的继承:不同类中含有多个重定义的字段或者方法,首先类之间直接使用冒号+基类名即可,但是由于构造函数是必须要的,所以在继承之前,构造函数要么都是无参,要么派生类中构造函数的参数能满足基类的构造函数参数方可继承,那么不同类之间,派生类的构造函数这时候依旧使用冒号 : ,但是后面的关键词不一样了,因为是不同类之间,而且要和父类继承,所以关键词也很贴切的使用了base,这也就是上面所说的 :base()。
一些收获心得:类继承说是说继承,其实和重载的核心是一致的,都是把累赘的代码优化到一个类中,而构造函数是堆到同一个函数中去,省去了重复写代码的过程。
总结: 首先需要继承一个无参父类的话,只需要将类的名字右边加上父类即可完成继承关系。若是由参数的父类的话,子类想要继承父类,就要包含父类的参数,通过构造函数进行传参,但是需要关键字bas意思是基于父类,然后通过构造函数的参数,直接把参数写到base括号内即可,虽然在base中不用谢类型名,但是要对应好类型位置。
父类与子类的代码:
public class Person
{
public Person(string name)
{
this.Name = name;
}
private string _name;
public string Name
{
get { return _name; }
set { _name = value;}
}
public void Person_Say()
{
Console.WriteLine("你好我不是人。");
}
}
public class Handsomeboy : Person
{
public Handsomeboy(string name) base : (name)
{
this.Name = name;
}
private int _name;
public int Name
{
get { return this._name; }
set { this._name= value; }
}
public void Handsomeboy_Say()
{
Console.WriteLine("我是{0}靓仔喔。",this.Name);
}
}
new
protected
假设现有一个父类 Person 和一个子类 Student
创建对象 : Person p = new Person();
里氏转换: p = new Student();
注意的是:即使你把子类内容new给了父类p,但是在调用p的时候还是不能访问到Student子类中的对象,只有使用强制转换的语法能调用到。
如: ((Student)p).方法/成员,这样用括号括起来的时候才能够将Student的方法或者成员进行调用到。
创建集合对象 :ArrayList list = new ArrayList();
创建的方法和正常的创建对象是一模一样的。
创建完成后,该集合对象存放任何类型,也就是object类型,就是任意的,所以在集合中经常会使用到里氏转换,我们可以存放不同的类型进一个集合中,因为任意类型就相当于一个父类,那么子类就可以是随便的一个指定类型。不知道你能不能理解到位。
集合:数据很多,类型可以很多,能存放很多类型。最重要的是一个集合的长度是可变的,所以只要你存放的东西进去,内存用完了就会继续给你开辟空间。特点就是长度可以任意改变,类型随便。
数组:类型单一,长度不可变,定义多少就只能存多少。
下面继续用刚刚创建的对象 list。
一: 增加单个元素
list.Add();
在list数组中增加元素,括号里面可以是任意类型,所以我们直接把元素类型放进去就好。
如:list.Add(1) / list.Add(“张三”)
当我们存放了很多个类型的时候,list就是一个集合数组,只是数组元素的类型不一样,同样我们可以将他当作数组一样使用即可
list.Count
表示的是集合存储的个数,和数组的关键词不一样而已,同样是自带的计算出个数的。
示例代码:
list.Add("张三");
list.Add(3.14);
list.Add(500m);
list.Add('女');
list.Add(true);
for(int i = 0; i < list.Count; i++)
{
Console.WriteLine(list[i]);
}
Console.ReadKey();
因为是object类型,所以是可以是任意,因此我存放一个数组类型,把数组类型看成单个整体,存放方式如下:
list.Add(new int[] {1,2,3,4,5,6}) / list.Add( 直接放一个开创好的数组名 )
如果直接存放数组进去的话当你打印的时候是打印不出来数组里面的内容的,因为没有什么打印的占位符是给数组的,数组不过是类型的元素集合。
所以我们在打印的时候需要将其先强制类型转换一下,记住,我们存什么类型进去,强转的时候就转什么类型,数组就强转数组,不要强转成数组元素类型,代码如下:强转方式:( ( int[] ) list[0] )
static void Main()
{
ArrayList list = new ArrayList();
int[] nums = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
//直接初始化数组,并且存多少,数组长度就为多少
list.Add(nums);//添加的是数组,直接给数组名即可
for (int i = 0; i < nums.Length; i++)
{
Console.Write(((int[])list[0])[i]+"/");
}
Console.ReadKey();
}
运行结果是:

如果是一个类存进了集合里面,我们就要用里氏强转规则,也就是将集合的类对象元素强转成为该类对象的类,然后才能使用类对象成员。
代码如下:
Person类代码 ↓ :
public class Person
{
public void Say()
{
Console.WriteLine("你好。hello.");
}
}
使用Person类对象存进集合里面 ↓ :
ArrayList list = new ArrayList();
Person per = new Person();
list.Add(per);
((Person)list[list.Count-1]).Say();
Console.ReadKey();
集合甚至还能将自己包含进去。
lisi.Add(list).这个是不会报错的,因为集合本身也是一个类型,所以也能把自己存进去。
但是有一个弊端就是访问的时候,不能访问自己,
目前我还没有找到单个元素存自己的访问方法(待定区域。。。)
解决访问自己的办法是下面的增加多个元素的方法。
总结:只要看成单个元素,就能存放进去,不过拿出来使用的时候要拆开成多个元素。
二: 增加多个元素
ArrayList list = new ArrayList();
list.AddRange(new int[] {1,2,3,4,5,6});
for(int i=0;i<list.Count;i++)
{
Console.Write(list[i]+"/");
}
如果这时候我将集合自己添加自己,用list.AddRange(list)的方式进行存储的话,我们就相当于把自己拆开当成了一个数组存放在在自己的下一个空间。有点晦涩难懂,访问的时候其实就his相当于访问了自己两遍。
代码:
ArrayList list = new ArrayList();
list.AddRange(new int[] {1,2,3,4,5,6});
//添加自己
list.AddRange(list);
for(int i=0;i<list.Count;i++)
{
Console.Write(list[i]+"/");
}
运行结果:
就是相当于将自己本来的元素作为一个集合形式,全部拆开后又全部存了一遍进去,所以打印出来的时候相当于打印了两遍的样子。

三:插入
插入单个元素
插入多个元素
四:删除元素
删除单个元素
根据元素名进行删除:
list.Remove(输入删除的元素);
括号里面输入删除的元素的和你添加他的时候的样子一样就行。
根据元素下标进行删除:
list.RemoveAt(待删除的元素下标)
根据元素下标范围进行删除:
list.RemoveRange(int startindex, int endidex)
五: 清空
移除元素,没有返回值,如果使用了该函数,集合中所有元素将会被全部清空。
六: 排序
七: 反转
八:判断集合内是否包含该元素
九:计算集合现在的总空间
Hashtable ht = new Hashtable();
ht.Add(1, "jackson");
ht.Add(2, true);
ht.Add("新添加的", 54);
因为hashtable仍然是一个集合,所以遍历的时候依旧是以遍历数组的形式进行访问集合内的元素,但是不同的不再通过下标进行访问,而是通过键来对应的访问。第一个参数就相当于一个路标,用该路标可以访问到对应的右边存放的元素。
如何遍历:
这时候想要遍历Hashtable集合一般要用foreach循环,但是如果你的键是按照下标的形式存放的话也可以用一般的循环来遍历,但是既然用到了Hashtable就一般都是想要用特殊的键来访问元素,所以我们要用foreach循环。
foreach : 循环参数(var item in 对应的集合)
解释:用item替换集合里面的每一个元素,每循环一次都用item来替代对应集合里面的元素。这时候可以利用foreach的特性,用Hashtable的键集合放进去,然后用item代替,输出ht[item]即可。
注意的是item不是固定的,只是var类型的一个命名而已。
var : 能够把传进来的集合转化为对应的类型,然后就能用item接收该元素,然后再进行输出,这个和拆箱类似,但是他没有执行拆箱,所以他不会消耗性能。
代码如下:
Hashtable ht = new Hashtable();
ht.Add(1, "jackson");
ht.Add(2, true);
ht.Add("新添加的", 54);
foreach (var it in ht.Values)
{
Console.WriteLine(it);
}
Console.ReadKey();
泛型集合有什么用?
大大避免了前面两个集合中操作发生的装箱和拆箱(装拆箱后面有讲,就是会拖慢电脑性能的一个东西),那么泛型就是不一样,他参数是T,代表泛型,不是object。
List<string> str = new List<string>();
str.Add("张三");
str.Add("李四");
str.Add("王五");
for(int i = 0; i < str.Count; i++)
{
Console.WriteLine(str[i]);
}
装箱和拆箱只发生在继承关系中,而且只有发生了装箱才能发生拆箱。
上述讲到的集合就是在做装箱和拆箱,因为object是所以类型的父类,所以在转换的时候总能将object能够拆箱,转化为对应的类型。
但是装箱和拆箱相当消耗性能,耗时间,所以之后就很少用这些甚至是不用。
重要:但是在List泛类型中,参数却不是引用类型,T是泛型的默认值,也就是说不是引用类型,那么前辈们为了解决重复装箱和拆箱操作而拖慢电脑,后面就不使用arraylist集合和hashtable集合,后面就使用了List泛型集合,因为泛型不会发生装箱和拆箱,这暂时没有深入研究为什么,具体我后面有空了就写一个博客。(待定)
用途:当想用一个方法实现多种不同效果的时候就可以用多态。
虚方法解释:用父类的方法作为虚拟的,当子类继承了父类之后,想要用同一个方法名进行不同的实现方式的时候,我们在调用父类的方法的时候会自动跳到父类中存放的该子类的方法里面进行实现,因为之前说过,子类继承了父类之后,父类对象可以通过子类进行赋值,也就说父类表面上是父类,但是通过虚方法之后,会跳到存放的对应子类的方法进行调用,这就是多态。
如何操作:首先要对父类和子类进行一个继承关系,继承完成后,因为我们的多态就是要通过一个方法名来实现不同的效果,这时候就要把父类的同名方法加一个virtual关键字,子类同名方法中加一个override,意思就当创建了一个父类对象的时候,我们存的是子类的空间给了该对象,那么在调用同名方法的时候就会自动调用到子类的方法而不是像之前直接继承的时候,父类只能调用到父类,或者通过里氏转换才能调用到该父类对象存放的子类方法。因此虚方法极大的简便了实现多态的过程中写更多重代码。
注意: 在我实现虚方法的时候遇到的困难是,在父类中想要传参一定要在构造函数中传,父类构造函数传参,我们在的子类也要进行构造函数的传参,用base基于父类将参数传给父类实现,然后在调用同名方法的时候再跳到子类方法运行。在子类中实现多态(同名)方法的前,是从父类方法的虚方法中跳过来的,override就是一个将父类中virtual中的方法进行重写。
个人理解:在调试过程中我发现,使用父类类型创建子类对象的时候, 如果我们new的子类对象有参数,会把参数首先传进子类的构造函数中,然后在子类构造函数通过base把参数传到父类中,然后在父类构造函数中进行属性赋值和限制参数的格式,然后就是开辟成功。
当我们调用方法的时候,首先会进入父类同名方法中,然后再通过判断该子类是哪一个,进入该子类的override函数中进行重写,这样就调用到了子类对象的方法。就是通过父类空间存储子类的东西,然后表现出来的就是一个类型表现出多个不同的状态,这就是我所理解的多态。
❀ 在调用override的时候,有一个base.方法,这个是通过在子类方法中调用回父类的虚方法函数(如果你需要的话)。
总结:通过学习虚方法和调试过程中,我对public这个修饰符更加深刻,通过传参的时候,因为我们的属性是public,所以在调用子类的重写函数的时候,我们依旧可以调用到父类的属性字段,也就是时候,我们在继承过程中,子类所有和父类重复的参数,我们都把他堆到父类一个属性中完成赋值,所以我们在子类中实现的时候直接调用父类的属性即可,那个即是我们子类传进去的参数,就是属性位置
虚方法实现代码如下:
class Program
{
static void Main()
{
#region 虚方法实现多态
virtual_Person[] zhang = new virtual_Person[3]
{
new virtual_Student("爱学习的学霸"),
new virtual_Teacher("教书的老师"),
new virtual_Person("不想学习的学渣")
};
for (int i = 0; i < zhang.Length; i++)
{
zhang[i].Vir_SayHello();
}
#endregion
Console.ReadKey();
}
}
public class virtual_Person
{
public virtual_Person(string name)
{
tihs.Name = name;
}
private string _name;
public string Name
{ get { return _name; }
set { _name = value; }
}
public virtual void Vir_SayHello()
{
Console.WriteLine("你好,我是{0}。",this.Name);
}
}
public class virtual_Student : virtual_Person
{
public virtual_Student(string name) : base(name)
{
}
public override void Vir_SayHello()
{
//base.Vir_SayHello();
Console.WriteLine("你好你好,我是学生{0}",this.Name);
}
}
我对抽象类的解释有如下几点:
需要注意的点:
多态的前提是继承,只有存在继承关系了,各个类之间才能建立联系。
首先我们想要使用多态是因为我们想要实现现实生活中,一个类别能表现出来很多种可能
比如说动物叫:猫叫,狗叫,鸟叫,等等,我们在代码中想要用一个类的一个方法就能实现多种不同的叫法就是多态,但是想要实现这种情况就要抽象出来一个类那就是动物类。
那么我们抽象出一个动物类之后很显然我们是要用你抽象类实现多态,这是我们一般比较多使用的情况。
如果我们抽象不出来一个类,我再打个比方说:走路。
很明显我们抽象不出来一个对象,也就是说世界上走路的方式有很多,动物人类就有很多不同,这时候我们就要用虚方法,虚方法只是针对方法,将走路虚化,然后在不同类中实现的时候只要将该方法重写就可以使用,而且都是用走路这个方法名,这也是多态,用一个方法名实现多种不同的走路方式。
在C#中使用文件流操作是一个比较强大的功能,能够通过字节的复制将任何文件。
第一种,需要手动尽心关闭文件和清除,代码如下:
#region 写入文件(手动关闭文件流,没有使用using)
//打开文件
FileStream fileread = new FileStream(@"D:\桌面\old.txt",FileMode.OpenOrCreate,FileAccess.Read);
byte[] buffer = new byte[1024*1024*5];
int de = fileread.Read(buffer, 0, buffer.Length);
string str = Encoding.UTF8.GetString(buffer);
//关闭流
fileread.Close();
//清除流占用的空间
fileread.Dispose();
Console.WriteLine(str);
#endregion
第二种,不需要进行关闭文件和清除文件流占用的空间,代码如下:
using(FileStream filewrite = new FileStream(@"D:\桌面\new.txt", FileMode.OpenOrCreate, FileAccess.Write))
{
filewrite.Write(buffer, 0, buffer.Length);
}
文件流的实操:复制mp3音频到另一个文件夹
class Program
{
public static void Main()
{
#region 实现天天酷跑音乐mp3的复制
string source = @"D:\MY C#\3:面向对象多态\文件操作\音频文件\source\SNH48 - 酷跑Run To You.mp3";
string target = @"D:\MY C#\3:面向对象多态\文件操作\音频文件\target\SNH48 - 酷跑.mp3";
Copy(target, source);
Console.WriteLine("复制成功");
Console.ReadKey();
#endregion
}
public static void Copy(string target, string source)
{
using (FileStream music = new FileStream(source, FileMode.OpenOrCreate, FileAccess.Read))
{
using (FileStream cpymusic = new FileStream(target, FileMode.OpenOrCreate, FileAccess.Write))
{
byte[] buffer = new byte[1024 * 1024 * 5];
while(true)
{
int r = music.Read(buffer, 0, buffer.Length);
if(r == 0) break;
else
{
cpymusic.Write(buffer, 0, r);
}
}
}
}
}
}
运行结果截图:复制成功
两个文件夹,实现从source中的音频复制到target中

source文件夹

target文件夹
实现成功,target中已经复制过来了。
注意:一定要在同一个盘里面进行操作,否则会报异常。
道友历尽艰辛终于学完了最最基础部分,并且我写的内容十分有限,很多细节没有补充上去,仅仅代表个人观点,认为比较重要的,较常用的写上去了而已,望各位道友多多担待~
如有错误还请各位道友私信我,指出错误,我会修改一番再拿出来供各位道友观看。
劝君更尽一杯酒,西出阳关无故人,本散仙就此告别。