装箱和拆箱
概念&描述
- 装箱:将值类型转换为引用类型
- 拆箱:将引用类型转换为值类型
不论是装箱还是拆箱,效率都较为低下,因此在实际项目中应当尽量避免大规模的出现装箱&拆箱代码
案例
为了更好理解装箱和拆箱使用以下一个案例来理解:
using System;
namespace _BoxExample
{
internal class Example
{
static void Main(string[] args)
{
//装箱
int x1 = 1;
object y1 = x1;
//拆箱
int x2 = (int)y1;
}
}
}
触发条件
只有当两种类型存在继承关系时,才有可能发生装箱&拆箱操作
例如:
string s = "1221";
int n = Convert.ToInt32(s);
将string转为int并没有发生装箱与拆箱操作,因为他们不存在继承关系
当发生案例情况时,int类型(子)与object(父)是继承关系,因此触发了装箱与拆箱操作
字典(键值对集合)
使用字典集合必须调用System.Collections.Generic
才能运作
以下是创建一个字典集合的案例:
using System;
using System.Collections.Generic;
namespace _DictionaryExample
{
internal class Example
{
static void Main(string[] args)
{
Dictionary<int,string> adic = new Dictionary<int,string>();//创建一个字典集合对象
}
}
}
这样我们创建了一个名字为adic
的字典集合
作用
使用字典集合有一个很好的好处,能够控制键和值的类型,比如举例创建的对象便是要求adic
集合的键一定是int
类型,而值一定要是string
类型
这样能够方便的管理对象的键,让其内容控制在合理范围内
调用方法
各类方法(函数)与HashTable一致,包括Add()
与通过键赋值等
但是其遍历方式可以有些不同
遍历访问
同样使用foreach()
循环,但是参数不一样
通过KeyValuePair<>
来获得adic
字典的键与值
using System;
using System.Collections.Generic;
namespace _DictionaryExample
{
internal class Example
{
static void Main(string[] args)
{
Dictionary<int,string> adic = new Dictionary<int,string>();//创建一个字典
adic.Add(1, "121");
adic.Add(2, "889");
adic.Add(3, "198");
adic[1] = "233";
//普通在Hashtable中使用的foreach遍历方式
foreach (var item in adic.Keys)
{
Console.WriteLine("{0}>{1}", item, adic[item]);
}
//同样使用foreach但是不同参数的遍历方式
foreach (KeyValuePair<int,string> kvp in adic)
{
Console.WriteLine("{0}-->{1}", kvp.Key, kvp.Value);
}
}
}
}
FileStream文件流
前面使用File类对文件进行操作都是一次性的,对内存会造成较大负荷,因此需要使用FileStream来进行这些操作
概念/区别
FileStream需要搭配StreamReader&StreamWriter使用
- FileStream用于操作字节
- StreamReader&StreamWriter用于操作字符
File与FileStream的区别
以两个水缸举例,一个水缸的水要倒到另一个水缸,有两种方法(固定思维):
- 一勺一勺的挖,不费力但是需要长时间——FileStream
- 直接扛起来倒入另一个水缸——File
上手
创建FileStream需要调用System.IO
创建对象(FileStream)
通过以下代码来创建一个实例对象
using System;
using System.IO;//调用需要的命名空间
using System.Text;
namespace _FileStreamExample
{
internal class Example
{
static void Main(string[] args)
{
FileStream FSRead =
new FileStream(@"D:\64\FileStreamExample_01.txt",FileMode.OpenOrCreate,FileAccess.ReadWrite);
//设置访问路径,选择打开方式,需要进行的数据操作
}
}
}
其中
FileStream FSRead = new FileStream(@"D:\64\FileStreamExample_01.txt",FileMode.OpenOrCreate,FileAccess.ReadWrite);
的构造函数
FileStream(string path,FileMode,FileAccess)
分别设置了文件所在路径、打开的方法与文件数据处理方法(权限)
设置读取缓存区(FileStream)
通过Read()
方法来设置读取缓存用的字节数组等参数,并且会返回在本次读取中实际占用的有效字节数
//设定读取缓存区域
byte[] FSRead_Cache = new byte[1024*1024*5];//设定缓存字节数组空间为5M
/设置了缓存用的字节数组、在字节数组中写入的起始偏移(通常为0)与最大缓存区域
FSRead.Read(FSRead_Cache,0,FSRead_Cache.Length);
而后这个方法会返回一个int类型数据
将字节数组中的内容解码(Encoding)
通过Encoding
将字节数组的内容解码并存入字符串中
string s = Encoding.Default.GetString(FSRead_Cache,0,r);
关闭文件流和释放资源(FileStream)
通过Close()
方法关闭文件流,Dispose()
方法释放资源
FSRead.Close();//关闭文件流
FSRead.Dispose();//释放资源
写入文件与自动关闭&释放资源(FileStream&using)
做到自动关闭与自动释放资源需要让整个FileStream在using(){}
框架内
以下是一个例子:
//通过FileStream写入文件,并且通过using自动关闭
using(FileStream FSWrite = new FileStream(
@"D:\64\FileStreamExample_01.txt", FileMode.OpenOrCreate,FileAccess.ReadWrite)
)
{
//准备写入的内容
string need_write = "12138";
//设定缓存字节数组空间为5M
byte[] FSWrite_Cache = Encoding.UTF8.GetBytes(need_write);
FSWrite.Write(FSWrite_Cache,0,FSWrite_Cache.Length);
}
Console.WriteLine("Write Success");
StreamReader & StreamWriter
StreamReader
通过以下代码新建对象
StreamReader readAfile = new StreamReader(@"D:\64\S1.txt",Encoding.Default)
使用Encoding.Default
来确保编码正确
StreamWriter
通过以下代码新建对象
StreamWriter writeAfile = new StreamWriter(@"D:\64\new.txt")
可以在创建对象时确认是否要在写入时覆盖源文件还是叠加源文件
StreamWriter writeAfile = new StreamWriter(@"D:\64\new.txt",true)//覆盖
多态
- 概念:让一个对象表现出多种状态(类型)
举例
- 先创建一个父类,里面要有一个方法、一个字段和一个带参数构建方法,而后创建2个以上基于该父类的之类,除了构建方法带上
:base()
以外,同时写一个具有标志性的方法与父类方法重名
//创建子对象
SubExample s1 = new SubExample("hi");
SubExample s2 = new SubExample("hi2");
TrdExample t1 = new TrdExample("hello");
TrdExample t2 = new TrdExample("hello2");
//将其放入父类数组
Example[] es = {s1,s2, t1, t2};
如果在一般的时候,将这些对象放入父类的数组中,其读取出来都是父类的对象,调用也是父类的资源,需要通过以下代码强转后调用
for (int i = 0; i < es.Length; i++)
{
if(es[i] is SubExample)
{
((SubExample)es[i]).WhoAmI();
}else if(es[i] is TrdExample)
{
((TrdExample)es[i]).WhoAmI();
}
}
这样未免过于繁琐了,因此需要以下方法实现一个对象多种状态以供调用
实现多态
实现多态有三种方法
- 虚方法
- 抽象类
- 接口
虚方法
- 解决方案:在调用状态为父类的子类对象(即意为改对象会调用父类的方法而不是子类的方法)方法,让其父类的方法自动调用子类的方法即为虚方法
实现方法
- 方法实现方法类方法标记为虚方法,使用关键字
virtual
,即意为允许子类将该方法重写 - 在要覆写的子类方法写入关键字
override
,即表示子类会将该同名的并且标记了virtual
的方法重写
抽象类
由于部分子类不适合做父类(比如猫和狗,都有叫的需求,但是两个都不适合做父类)时,可以使用抽象类来实现多态
当然也可以用于在父类中的方法不清楚要哪些时,可以将父类写成抽象类,将方法写成抽象方法
- 抽象方法存在的意义是为了在父类无法确认(不知道父类的意义时)如何实现方法时让子类重写方法
抽象类禁止创建对象(无意义因此禁止)
- 解决方案:在通过
Animal(一个抽象父类) aDog = new Dog();(Dog是一个子类)
创建对象时,Dog属于Animal类,并且调用的Brak()
——在父类中的抽象类方法和子类的重名方法(并且已使用override标记覆写)时,依旧调用的是Animal的抽象方法,但是由于是抽象方法并且有override标记因此被覆写掉是Dog中的Brak()
方法
实现
- 可以将一个类标记为抽象类,在创建类时通过
abstract
关键字标记为抽象类 - 方法也是通过
abstract
关键字标记为抽象方法
注意,抽象方法不允许有方法体
抽象类与虚方法的对比
抽象类和虚方法的区别
- 虚方法的父类已经有了实现,而抽象类的父类中的重名方法没有实现
抽象类
- 抽象类可以创建非抽象类属性和方法与构造函数,不过由于其是抽象类因此无法创建对象,但是这些属性和方法可以提供给子类使用
- 抽象成员只能存在于抽象类中,并且不可
private
- 抽象类不可实例化
- 当子类也是抽象类时子类不需要实现父类方法和成员
- 抽象类中的方法在正常子类中必须实现,并且如果有返回值&传入参数也必须拥有
抽象属性
在抽象类中新建属性时使用abstract
标记即可生成抽象属性
抽象类与虚方法
抽象类能够与虚方法兼容,在抽象类中可以创建一个虚方法,并且虚方法可以写方法体,两者可共存与覆写
关于接口
接口请参照接口部分
访问修饰符补充
通过访问修饰符来控制哪些位置可以读取这个属性
例表
访问修饰符 | 可用于 | 可被哪些位置访问 |
---|---|---|
public | class,other | 解决方案中的任意位置 |
internal | class,other | 可被当前程序集 (in Java is Package) 访问 |
protected | other | 只能被当前类内部和该类子类访问 |
private | other | 只能被当前类内部访问 |
子类的访问权限不能高于父类的访问权限 ——因为会暴露父类成员
设计模式
- 设计项目的方式,用于解决项目中的难题,共计有23种设计模式
简单工厂设计模式
以生成笔记本为例,笔记本分为各种牌子,而只有用户知道用户需要哪个牌子的笔记本,但是工厂不能停工,为了适应用户需求可以一直生产笔记本(各个牌子的父类),用户选择后将子类笔记本其打包在父类对象中给用户
案例
具体参考06-工程设计模式项目
值传递和引用传递
- 值:例如
int
/double
/char
/bool
/enum
等类型属于值 - 引用:例如
string
/数组
/object
/集合
/自创建对象
/接口
等
值类型会存储在栈内,引用类型会存储在堆内
值传递
一次标准的值传递
int n1 = 10 ;
int n2 = n1 ;
n2 = 20 ;
Console.WriteLine("{0}\n{1}"n1,n2);
控制台将输出
10
20
其中n1的值是存储在栈内,而n2是从n1复制然后存储在一个新的栈内,因此传递的是这个值本身而不是n2指向n1
引用传递
而在引用类型是另一种情况,按以下代码为例
//Person是一个预先写好的类,有name这一个属性,并且name是公开的
Person p1 = new Person();
p1.name="AB";
Person p2 = p1;
p2.name="CD";
Console.WriteLine(p1.name);
此时控制台将会打印
CD
由于引用类型在复制的时候,传递的是对这个对象的引用
classDiagram
栈<|--堆
class 栈{
+p1 = person在堆中的地址
}
class 堆{
+new Person
}
当执行到Person p2 = p1;
时
classDiagram
栈<|--堆
class 栈{
+p1 = person在堆中的地址
+p2 = 与p1同样的地址
}
class 堆{
+new Person
}
当p2.name="CD"
时,堆中对应的地址被修改,而由于p1和p2共用一个地址,因此p1.name
也被修改
但是String是例外,由于字符串是不可变性变量,因此在执行上述自定义类的操作时,String并不会发生改变
ref关键字
众所周知在值传入方法后,如果在方法内改变而不返回返回值的话外部的变量依旧不会变,因为他们在栈中地址不一样
而在值使用ref
时并调用方法后,两个值(外部变量和方法内的变量)在栈中的地址便是一样的了
序列化与反序列化
- 序列化:将对象转换为二进制
- 反序列化:将二进制转换为对象
- 作用:传输数据
序列化
- 如果需要序列化对象,需要将该类标记为可序列化的对象,需要在类的上一行标记
[Serializable]
,而后该类禁止被继承 - 开始序列化前还需要调用
System.Runtime.Serialization.Formatters.Binary;
来引入相关方法与对象 - 而后新建一个
BinaryFormatter
对象,通过Serialize
方法将其序列化 - 噢,
Serialize
还需要一个Stream
对象用于传输与存储,因此可以新建一个FileStream
对象来存储,记得填地址和给权限 - 而后运行,将会写入与文件流对象实例化的文件
反序列化
- 依旧需要序列化对象
BinaryFormatter
,而后调用Deserialize
方法,然后会返回一个Object类型 - 通过
变量名 = (要强转的类型)BinaryFormatter对象名.Deserialize(FileStream流)
来读取对象并转换为相应类型
部分类
当在同一namespace 命名空间
下时,会由于共同开发等原因需要写重名类,若要避免重名造成的名称冲突可以在类前加上关键字partial
特性
- 两个同名类被打上
partial
标签后,可以共存 - 这两个类可以互相访问所有字段,包括
private
权限的变量 - 部分类内依旧禁止有重名但是不重载的方法
密封类
- 在类前加入关键字
sealed
即可标记该类为密封类
特性
- 禁止继承(派生)
- 密封类可以继承其它非密封类
重写ToString()
方法
由于正常情况下ToString()
方法转换为String
后是直接打印对象名称
因此需要重写ToString
方法来使用
与
ToString()
方法一样基于Object
类可能需要重写的方法还有Equals()
方法和GetHashCode()
方法
接口
接口是实现多态的第三种方法
适用案例
- 例现有三个类,分别是Person、NBA Player、Student,Student继承于父类Person而NBA Player是单独一个类,此时Student类想要使用NBA Player类中的方法,此时除了让NBA Player是Person的子类然后Student是NBA Player的子类以外,还能由NBA Player这个类提供接口用于重写
如何实现接口
按以下代码为例
public class Person
{
public void SayHello()
{
Console.WriteLine("Hi");
}
}
public class NBAPlayer
{
public void PlayBasketball()
{
Console.WriteLine("Bong");
}
}
public class Student:Person
{
public void Study()
{
Console.WriteLine("Study");
}
}
Student已继承Person类,此时想要NBA Player的PlayBasketball()
方法
添加interface
public interface IPlayBasketballable{
void PlayBasketball();
}
而后在Student类中添加接口
public class Student:Person,IPlayBasketballable
再在Student类内重写PlayBasketball()
方法
public void PlayBasketball(){
....
}
为何是接口
- 接口一般用在已有父类的情况下还需要更多方法来重写,此时即可使用接口
- 接口也是一种规范、能力
语法
通过以下方法声明一个接口
[public] interface (I(名称)able){
成员
}
- 接口中的成员不允许添加访问修饰符,默认为
public
- 接口中的方法没有方法体,也不能包含有方法体的方法
- 接口中不能有字段,但是可以有自动属性
特性
- 如果有类继承了接口则这个类必须实现接口的所有属性
- 接口不可被实例化
- 接口不能继承类(类可以继承接口),但是能继承多个接口,并且不用在新的接口中重写
显示实现接口
当类内有方法与接口内的方法重名时,会被认为类内重名的方法是接口的方法重写,此时需要指明是接口的方法进行重写
- 以之前的Student、NBA Player与Person为例,新建Teacher类
public class Teacher : Person, IPlayBasketballable
{
public void PlayBasketball(string height)
{
Console.WriteLine(height);
}
public void IPlayBasketballable.PlayBasketball(string height)
{
Console.WriteLine("too");
}
}
通过接口名.方法名
来重写特定接口的方法
若要调用接口内的方法,可以通过创建接口对象,实例为子类的初始化来使用接口内方法
自动属性
通过以下方法声明一个自动属性
string name{
get;
set;
}
自动属性即为自动生成私有字段,并且写法不同,没有字段没有方法体,因此也不允许限定属性 (因为少了限制的方法体)
GUID
GUID能产生一个独一无二的编号
Guid name = Guid.NewGuid();
//or
Guid.NewGuid();
name.ToString();//转字符串
以上即可
MD5
如何进行MD5加密,通过以下方法即可
public static string GetMD5(string str)
{
//创建MD5对象
MD5 md5 = MD5.Create();
//加密
//先将string转为字节数组
byte[] buffer = Encoding.Default.GetBytes(str);
//返回已加密的字节数组
byte[] result = md5.ComputeHash(buffer);
//再将字节数组转为字符串
string reback = "";
for (int i = 0; i < result.Length; i++)
{
reback += result[i].ToString("x");
}
return reback;
}
需要注意
- md5加密后的字节数直接将其中元素使用
toString()
转换即可,不需要进行转码,但是MD5加密需要将字符串转成字节数组 - 在
ToString()
方法中填入字符串"x"
即可将其转为16进制内容(默认10进制)
2 comments
啥时候换这个域名了?我博客挂的友链还是之前的,还以为你关了
我之前弄了重定向
但是好像没有工作((