.NET Book Zero 读书笔记(三)(从C++的使用者角度学习C#)

Interfaces

Interface看起来像是Class或者Struct,但是里面的method都没有body,比如:

// Interfaces are entirely overhead! They contain no code.
public interface IComparable
{
	int CompareTo(object obj);
}

.NET Framework里的接口都是I开头的,但是这也只是一种命名习惯,Interfaces里也可以有Property,因为Property其实也是编译器会生成的函数,但是不可以有Body。

注意,一个类只可以继承于一个类,但是可以继承多个Interface

举个使用的例子,通过实现IComparable接口可以让,Array.Sort这个static函数对该类的对象进行排序,比如对如下类:

partial class SuperDate: ExtendedDate, IComparable
{
	public int CompareTo(object obj)
	{
		if (obj == null)
			return 1;
		if (!(obj is SuperDate))
			throw new ArgumentException();
		return this - (SuperDate)obj;
 }

然后就可以用Array.Sort函数对其排序了:

Array.Sort(mySuperDateArr);

注意,Array.Sort还可以接受两个参数的相同Size的数组,第一个数组的第i个元素和第二个数组的第i个元素会形成key-value的mapping关系,当排序第一个数组时,第二个数组也会随着key的改变而改变,感觉写起来很方便,可以看个例子:

using System;

class DateSorting
{
    static void Main()
    {
        string[] strComposers =
        {
            "John Adams", "Johann Sebastian Bach",
            "Bela Bartok", "Ludwig van Beethoven",
            "Hector Berlioz", "Pierre Boulez",
            "Johannes Brahms", "Benjamin Britten",
            "Aaron Copland", "Claude Debussy",
            "Philip Glass", "George Frideric Handel",
            "Franz Joseph Haydn", "Gustav Mahler",
            "Claudio Monteverdi", "Wolfgang Amadeus Mozart",
            "Sergei Prokofiev", "Steve Reich",
            "Franz Schubert", "Igor Stravinsky",
            "Richard Wagner", "Anton Webern"
        };
        SuperDate[] sdBirthDates =
        {
            new SuperDate(1947, 2, 15), new SuperDate(1685, 3, 21),
            new SuperDate(1881, 3, 25), new SuperDate(1770, 12, 17),
            new SuperDate(1803, 12, 11), new SuperDate(1925, 3, 26),
            new SuperDate(1833, 5, 7), new SuperDate(1913, 11, 22),
            new SuperDate(1900, 11, 14), new SuperDate(1862, 8, 22),
            new SuperDate(1937, 1, 31), new SuperDate(1685, 2, 23),
            new SuperDate(1732, 3, 31), new SuperDate(1860, 7, 7),
            new SuperDate(1567, 5, 15), new SuperDate(1756, 1, 27),
            new SuperDate(1891, 4, 23), new SuperDate(1936, 10, 3),
            new SuperDate(1797, 1, 31), new SuperDate(1882, 6, 17),
            new SuperDate(1813, 5, 22), new SuperDate(1883, 12, 3)
        };
        Array.Sort(sdBirthDates, strComposers);
        for (int i = 0; i < strComposers.Length; i++)
            Console.WriteLine("{0} was born on {1}.",
            strComposers[i], sdBirthDates[i]);
    }
}

最后输出是按照出生日期来排序的:

Claudio Monteverdi was born on 15 May 1567.
George Frideric Handel was born on 23 Feb 1685.
Johann Sebastian Bach was born on 21 Mar 1685.
Franz Joseph Haydn was born on 31 Mar 1732.
Wolfgang Amadeus Mozart was born on 27 Jan 1756.
Ludwig van Beethoven was born on 17 Dec 1770.
Franz Schubert was born on 31 Jan 1797.
Hector Berlioz was born on 11 Dec 1803.
Richard Wagner was born on 22 May 1813.
Johannes Brahms was born on 7 May 1833.
Gustav Mahler was born on 7 Jul 1860.
Claude Debussy was born on 22 Aug 1862.
Bela Bartok was born on 25 Mar 1881.
Igor Stravinsky was born on 17 Jun 1882.
Anton Webern was born on 3 Dec 1883.
Sergei Prokofiev was born on 23 Apr 1891.
Aaron Copland was born on 14 Nov 1900.
Benjamin Britten was born on 22 Nov 1913.
Pierre Boulez was born on 26 Mar 1925.
Steve Reich was born on 3 Oct 1936.
Philip Glass was born on 31 Jan 1937.
John Adams was born on 15 Feb 1947.

使用Interface时注意的东西

比如说有这么个Interface

public interface ITransform
{
    Vector3 position { get; set; }
    Quaternion rotation { get; set; }
    Vector3 scale { get; set; }
    Matrix4x4 matrix { get; }// (Read Only)
}

正常继承这个Interface时,这么写会报错:

public class A : ITransform
{ 
    Vector3 position { get; set; }
    Quaternion rotation { get; set; }
    Vector3 scale { get; set; }
    Matrix4x4 matrix { get; }// (Read Only)
}

报错信息为:
在这里插入图片描述

意思是这里继承于Interface的东西,应该是public的。这么理解也很正常,他既然是一个公用的接口,是public的也是合理的。

不过这里也有特殊写法,让它即使是private的也可以通过编译,就是通过an explicit interface implementation,代码如下:

public class A : ITransform
{ 
    Vector3 ITransform.position { get; set; }
    Quaternion ITransform.rotation { get; set; }
    Vector3 ITransform.scale { get; set; }
    Matrix4x4 ITransform.matrix { get; }// (Read Only)
}

参考:https://stackoverflow.com/questions/21783327/cannot-implement-an-interface-member-because-it-is-not-public

举个例子,参考https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/interface,这是对私有的接口的实现

interface ISampleInterface
{
    void SampleMethod();
}

class ImplementationClass : ISampleInterface
{
    // Explicit interface member implementation:
    void ISampleInterface.SampleMethod()
    {
        // Method implementation.
    }

    static void Main()
    {
        // Declare an interface instance.
        ISampleInterface obj = new ImplementationClass();

        // Call the member.
        obj.SampleMethod();
    }
}


Interoperability

Interoperability是交互性的意思,这一章主要讲C#是如何与系统API和其他语言的DLL进行交互的。

举个例子,这里调用系统的WIN32 API来获取当前的时间(尽管.NET的DataTime.Now可以提供这个功能),/一般的C#用于交互的API都在这个namespace下:

using System.Runtime.InteropServices;

这里调用的WIN32 API的函数签名的C语言风格的,代码如下:

// LPSYSTEMTIME是一个指向SYSTEMTIME类型的指针
void GetSystemTime(LPSYSTEMTIME lpSystemTime);

typedef struct _SYSTEMTIME
{
	WORD wYear;// WORD是一个16位的整型
	WORD wMonth;
	WORD wDayOfWeek;
	WORD wDay;
	WORD wHour;
	WORD wMinute;
	WORD wSecond;
	WORD wMilliseconds;
} SYSTEMTIME, *PSYSTEMTIME;// 

如果想要在C#这边调用这个函数,需要创建对应类型的指针,这里在C#里定义一个对应的类:

using System.Runtime.InteropServices;

// StructLayoutAttribute是命名空间下的类,用于描述类或Struct的Fields应该如何be interpreted
[StructLayout(LayoutKind.Sequential)]// 这玩意儿叫attribute
class SystemTime// 类名有点区别, 注意是class不是struct, 其实也可以是struct, 但是具体传的方法有点不一样
{
	public ushort wYear;// 名字相同,字节长度也相同,访问级别也相同
	public ushort wMonth;
	public ushort wDayOfWeek;
	public ushort wDay;
	public ushort wHour;
 	public ushort wMinute;
	public ushort wSecond;
	public ushort wMilliseconds;
}

除了使用LayoutKind.Sequantial,还可以使用LayoutKind.Explicit去为所有的fields来指定字节对齐的长度(give byte offsets for all the fields.)。

除此之外,要想调用系统的函数,还要把它作为dll的函数Import进来,代码如下:

// The attribute indicates the dynamic link library in which the function is stored.
[DllImport("kernel32.dll")]
static extern void GetSystemTime(SystemTime st);

最后的代码就是这样了,有意思的是,尽管系统的API要求传入的是指针,但是这里的函数直接传入的是class对象的引用,所以意味着C#里的函数引用本质就是指针?

using System;
using System.Runtime.InteropServices;
partial class SuperDate
{
    [StructLayout(LayoutKind.Sequential)]
    class SystemTime
    {
        public ushort wYear;
        public ushort wMonth;
        public ushort wDayOfWeek;
        public ushort wDay;
        public ushort wHour;
        public ushort wMinute;
        public ushort wSecond;
        public ushort wMilliseconds;
    }
    [DllImport("kernel32.dll")]
    static extern void GetSystemTime(SystemTime st);
    public static SuperDate Today()
    {
        SystemTime systime = new SystemTime();
        GetSystemTime(systime);
        return new SuperDate(systime.wYear, systime.wMonth, systime.wDay);
    }
}

用C#的struct对象作为系统API的参数
为什么之前用的class对象直接可以传入,但是struct不行,是因为前者在传参时是传引用,这里可以认为是指针,但是后者就是传值,所以,要想传struct,必须传它的引用,那么给它加上ref或者out关键字即可,由于out关键字传递的时候,不需要传递的对象实现被初始化,所以这里加个out即可,至于为什么ref不行,是不是因为控制权无法这样移交,还是怎么样,我还不是特别清楚。
总之改成这样就行了:

SystemTime systime;
GetSystemTime(out systime);

Attribute

Attributes are information you can attach to a type or member of a type. The information is stored as metadata along
with the compiled code.

Attribute可以添加到一个Type或者一个Type对应的成员上,它会在编译之后作为metadata一起被存储起来

PInvoke
WIN32的API里可能会有PInvoke类型的对象,在这个网站上做了相关介绍:
http://www.pinvoke.net


Dates and Times

这章没啥好说的,主要是介绍了.NET Framework里的一个时间类DateTime的用法,代码示例如下:

// 2007年8月29号15:30:00
DateTime dt = new DateTime(2007, 8, 29, 15, 30, 0);

// 获取各种时间信息
DateTime dtLocal = DateTime.Now;
DateTime dateToday = DateTime.Today;
DateTime dtUtc = DateTime.UtcNow;
// 还有Local、Utc和Unspecified三种时间类型
DateTime dtLocal = new DateTime(2007, 8, 29, 15, 30, 0, DateTimeKind.Local);

// 还有个TimeSpan类用于计算时间差
TimeSpan ts = dt1 – dt2;
TimeSpan ts = new TimeSpan(40, 30, 20, 10);
TimeSpan ts = new TimeSpan(4000,3000,2000, 1000);

最后还介绍了不同的Calendar的时间记录方法,看起来挺无语的:

// 一共这么多种类型
Object
Calendar
EastAsianLunisolarCalendar
GregorianCalendar
HebrewCalendar
HijriCalendar
JapaneseCalendar
JulianCalendar
KoreanCalendar
PersianCalendar
TaiwanCalendar
ThaiBuddhistCalendar
UmAlQuraCalendar

new DateTime(1900, 2, 29);//默认是Gregorian  calendar, 1900不是闰年,会报错
new DateTime(1900, 2, 29, new JulianCalendar());
new DateTime(5762, 5, 20, new HebrewCalendar());


Events and Delegates

这一章的内容在读书笔记一里提到了,就不再说了



Files and Streams

C#默认支持二进制文件、文本文件和XML文件的读写。

C#提供了两个命名空间,用于帮助读写文件:

  • System.IO: 可以用于帮助读写二进制和txt文件
  • System.Xml: 可以用于读写xml文件

System.IO命名空间下,不同文件对应的类是:

  • FileStream: 对应bytes
  • StreamReaderStreamWriter: 对应txt文件
  • BinaryReaderBinaryWriter: 对应bytes文件

file和stream的定义

书中提到,存储在磁盘上的,有路径和名字的文件,叫做file,而一旦这个文件被open用于读写之后,它就变成了一个stream。书里没有用数据流去解释它,而是说,一个Stream是你可以在上面实施读写操作的东西,但Stream不仅仅局限于文件上的东西,它可以是从网络上传输过来的一段数据,也可以是自己在Memory里创建的一块区域。在一个console用于里,keyboard input和text output都是streams


C#里的Stream类

C#的System.IO下面有个抽象类Stream,相关继承关系如下:
在这里插入图片描述
a stream is an object that lets you read bytes, write bytes, and seek to a particular location. 但不是所有的Stream都同时支持这些操作,Stream有四个属性:

  • CanRead:如果可读,那么可以对Stream对象调用ReadByte读取单个Byte,或者调用Read读取Byte数组
  • CanWrite:同上,可以调用WriteByteWrite,The Flush method writes any buffered output to the stream.(为啥Flush也是写)
  • CanSeek:比如用于磁盘上的文件读写操作,可以使用Length属性获取Stream的长度,还可以用Position属性设置当前pos,也可以查看当前读取的pos。Length和Position都为long类型,可以使用SeekEnd、SeekCurrent、SeekBegin函数(跟C++很像)
  • CanTimeout:比如用于网络传输的Stream,ReadTimeoutWriteTimeout属性可以设置timeout值,应该是设置掉线吧

额外的几点补充说明:

  • 如果可读、可Seek,那么可以调用SetLength函数
  • 可以使用BeginRead, EndRead, BeginWrite, and EndWrite methods to read or write the stream asynchronously(异步),不过这里的异步读写Stream我还不是特别清楚指的什么
  • 使用Close方法关闭stream

可以从上面的图中看到,C#的Stream是一个抽象类,它有四个派生的子类:

  • BufferedStream
  • FileStream
  • MemoryStream
  • NetworkStream

首先来说一说最常用的Stream: FileStream

FileStream类

FileStream继承于Stream类,可以执行最低级(rudimentary)的IO操作,创建FileStream的代码如下:

// 当构建文件失败时,会抛出IOException or FileNotFoundException,所以最好在try block里执行
FileStream fs = new FileStream(filePath, FileMode.XXX);

// 也可以指定第三种参数
FileStream fs = new FileStream(filePath, FileMode.XXX, FileAccess.XXX);

FileMode枚举参数
创建一个FileStream时,可以通过FileMode这个枚举指定创建Stream的相关文件操作,FileMode一共有六种模式:

  • FileMode.CreateNew: 创建新文件,将其变为Stream,如果路径对应的文件已经存在,那么会失败
  • FileMode.Create: 创建文件,将其变为Stream,如果路径对应的文件已经存在,那么会清空原本的内容
  • FileMode.Open: 打开指定文件,将其变为Stream,路径文件不存在,则失败
  • FileMode.OpenOrCreate: 打开指定文件,将其变为Stream,路径文件不存在,则创建对应文件 ,将其变为Stream
  • FileMode.Truncate: 清空指定文件内容,将其变为Stream,如果文件不存在则失败
  • FileMode.Append: 如果文件不存在,则会创建一个新文件,将其变为Stream;如果FileStream可以对文件进行Read操作,则失败(也就是说Append模式只可以写入文件);会把文件变为Stream,然后Seek到文件最后面

注意:上面创建FileStream的失败操作都会导致抛出异常

FileAccess枚举参数
除了指定Stream对文件的操作,还可以通过FileAccess枚举指定Stream访问文件的权限,FileAccess一共三种模式:读写、只读、只写:

  • FileAccess.Read: Fails for FileMode.CreateNew, FileMode.Create, FileMode.Truncate, or FileMode.Append。FileMode一共六种,其中四种都涉及到了写入(创建文件也算写入)所以这些模式如果是只读权限,则会失败
  • FileAccess.Write: 对于只读文件会失败
  • FileAccess.ReadWrite: 对于只读文件会失败,对FileMode.Append也会失败(Append是只写操作)

相关代码如下:

// 默认的FileAccess是ReadWrite, 一般可以不写
new FileStream(strFileName, FileMode.OpenOrCreate);

// Error, 因为Append是只写操作, 默认的FileAccess是不对的
new FileStream(strFileName, FileMode.Append);

// 正确, 一般只有Append模式下需要额外指定FileAccess
new FileStream(strFileName, FileMode.Append, FileAccess.Write);

FileShare枚举参数
可以通过这个枚举,在打开文件的时候,与其他的进程共享这个文件,具体共享的状态分为以下几种:

  • FileShare.None:不可分享(default)
  • FileShare.Read:可以一起读
  • FileShare.Write:可以一起写
  • FileShare.ReadWrite:可以一起读写

当两个File Stream要共享文件的时候,它们都必须在创建FileStream的时候,声明指定的FileShare枚举参数。

代码如下所示:

// Stream只可以读取文件时, 一般会允许其他的文件也来进行读取
// FileStream类内部会使用Lock和Unlock的methods来保护shared files
new FileStream(strFileName, FileMode.Open, FileAccess.Read, FileShare.Read);

FileStream的Property

  • CanReadCanWrite两个Property都依赖于构造函数里的FileAccess
  • CanSeek is always true for open files
  • CanSeek为true时,LengthPosition为valid,Length为只读的Property,而Position可读可写,可以用来设置读写的位置(单位是byte),LengthPosition,Both properties are of type long, which means they allow file sizes of up to 9 terabytes (9 × 10^9 bytes).

FileStream的Method

  • public override long Seek (long offset, System.IO.SeekOrigin origin):第二个参数是枚举(Begin, Current, and End)),代表offset的来源,与C的fseek函数类似
  • public override int ReadByte():读取单个Byte,将其转换为int返回,如果读到了end,则返回-1,读完Position会加1
  • public override int Read (byte[] array, int offset, int count):读取字节数组,第二个参数是传入Buffer的offeset,而count是读取的字节数,返回的int代表读取到buffer里的字节数,因为array的容量很可能大于实际读取的字节数,如果已经到了End,则返回0。比如:
byte[] buffer = new byte[1000];
fs.Read(buffer, 0, buffer.Length);
  • 至于WriteWriteByte,是类似的
  • 最后别忘了调用Closemethod

展示的FileStream的Demo

  • C++和C#的命令行调用exe的传参方式有所不同,C++需要传两个参数,一个是参数个数,一个是参数char*数组,而且第一个参数是exe的path,而C#只需要传一个参数,就是string[] strArgs,即String数组,如果没有输入任何参数,那么还是要输入exe的名字作为参数(应该传入的String数组第一个需要是filename吧?)
  • 这里有个16个字节转的方法,代码如下:
// 传入字节数组, count <= 16, buffer是一个size为16的数组
static string ComposeLine(long addr, byte[] buffer, int count)
{
	// X4里的X代表用16进制表示, 4代表展示4个digit
	// str的前面好像是打印出总的地址
	string str = String.Format("{0:X4}-{1:X4} ", (uint)addr / 65536, (ushort)addr);

	// 遍历16个byte
	for (int i = 0; i < 16; i++)
	{
		// 一个字节8位, 所以可以转换为两位的16进制数
		str += (i < count) ? String.Format("{0:X2}", buffer[i]) : " ";
		// 16个字节一共是32个十六进制的数字,这里在中间的第八个后面加了个分隔符号-
		str += (i == 7 && count > 7) ? "-" : " ";
	}
	
	str += " ";
	// 逐一把Byte转换为Char写入结果中
	for (int i = 0; i < 16; i++)
	{
		// 把Byte转换为Char
		char ch = (i < count) ? Convert.ToChar(buffer[i]) : ' ';
		// 特殊的Control字符特殊处理
		str += Char.IsControl(ch) ? "." : ch.ToString();
	}
	
	return str;
}

FileStream的缺点

  • 在C++里,可以直接把数据整个作为Byte数组读进来,然后取地址分别转换成自己想要的数据的指针;但是C#里做不到,还是得创建自己的数据结构,然后从里面一个个转换Byte得到,所以它不像C++里面读取FileStream那样灵活


StreamReader和StreamWriter

StreamReader是C#用于读取text文件的,相关的类继承关系如下图所示:
在这里插入图片描述
注意:虽然这些类不继承于Stream类,但是它们本质上还是通过Stream类实现的。

Text文件本身是挺简单的,但是由于Unicode的存在,让它变得复杂。C#里的char和string数据默认都是用的Unicode编码的。

StreamWriter的两类构造函数

// 1. 第一类, 通过打开文件创建StreamWriter
// 这些构造函数都会打开对应的文件, 底层应该也是创建了FileStream
// 如果想要保留文件的内容, 则append为true; size为buffer的size; 默认使用UTF8的编码
new StreamWriter(string filename)
new StreamWriter(string filename, bool append)
new StreamWriter(string filename, bool append, Encoding enc)
new StreamWriter(string filename, bool append, Encoding enc, int size)


// 2. 第二类, 通过Stream创建StreamWriter
new StreamWriter(Stream strm)
new StreamWriter(Stream strm, Encoding enc)
new StreamWriter(Stream strm, Encoding enc, int size)

默认是使用UTF8的编码,Encoding是个枚举:

Encoding.Default
Encoding.Unicode
Encoding.BigEndianUnicode
Encoding.UTF8
Encoding.UTF7
Encoding.ASCII

使用Encoding.Unicode作为编码的文件或者Stream,最开始的字符是0XFF和0XFE,也就是0XFEFF,是一种标记,it is
defined in the Unicode standard as the byte order mark (BOM)。还是不太了解,不多研究了(P234)


StreamWriter的例子

// 每次打开写入的时候, 就写入写入的时间
// 如果StreamWriterDemo.txt文件已经存在, 则会接着往后面write
// 否则之间创建StreamWriterDemo.txt文件
StreamWriter sw = new StreamWriter("StreamWriterDemo.txt", true);
sw.WriteLine("You ran the StreamWriterDemo program on {0}", DateTime.Now);
sw.Close();

StreamReader的两类构造函数

// 这里的detect用于detect文件的Encoding方式, 会从文件前面的2到3个bytes里进行识别
// 如果detect为true, 而且输入了Encoding, 那么会先去detect, 如果失败就会用输入的编码方式去读取
// 比如UTF7和ASCII两种编码就没有BOM, 而且bytes都在0x00 to 0x7F区间内

// 1.
new StreamReader(string filename);
new StreamReader(string filename, Encoding enc);
new StreamReader(string filename, bool detect);
new StreamReader(string filename, Encoding enc, bool detect);
new StreamReader(string filename, Encoding enc, bool detect, int size);

// 2.
new StreamReader(Stream strm);
new StreamReader(Stream strm, Encoding enc);
new StreamReader(Stream strm, bool detect);
new StreamReader(Stream strm, Encoding enc, bool detect);
new StreamReader(Stream strm, Encoding enc, bool detect, int size);

StreamReader的method

  • Peek和Read函数用于逐字符读取
  • ReadLine用于逐行读取
  • ReadToEnd直接读取到文件末尾

实际读文件的代码大概是这样:

StreamReader reader = new StreamReader(path);
string line;
while ((line = reader.ReadLine()) != null)  
{  
	Console.WriteLine(line);  
}    


Binary FIle I/O

一个文件,要么是text文件,要么就是Binary文件,相关的类的继承结构很简单:
在这里插入图片描述
二者的构造函数都需要传入一个Stream对象

BinaryWriter里提供了18个Write重载函数,一般的double、float这种类型的数据都是固定的长度的写入,对于String这种类型的二进制数据,会根据第一个字节的8位来判断,后七位代表string里字符的个数,最高位用来代表string是否还有更多的字符的个数,多的就不说了,不难。



Environment

System命名空间下有相关获取环境信息的类和函数

获取设备上的所有的磁盘的信息

// 代表C盘信息
DriveInfo info = new DriveInfo("C");
// 获取电脑上的磁盘信息
DriveInfo[] infos = DriveInfo.GetDrives();
// DriveInfo里有一个Property叫做DriveType, 是个枚举, Removable, Fixed, and CDRom

具体的代码如下:

DriveInfo[] infos = DriveInfo.GetDrives();
foreach (DriveInfo info in infos)
{
	Console.Write("{0} {1}, ", info.Name, info.DriveType);
	if (info.IsReady)
		Console.WriteLine("Label: {0}, Format: {1}, Size: {2:N0}", info.VolumeLabel, info.DriveFormat, info.TotalSize);
	else
		Console.WriteLine("Not ready");
}

在作者的电脑上打印如下:

A:\ Removable, Not ready
C:\ Fixed, Label: Windows XP Pro, Format: NTFS, Size: 52,427,898,880
D:\ Fixed, Label: Available, Format: NTFS, Size: 52,427,898,880
E:\ Removable, Not ready
F:\ CDRom, Not ready
// 
G:\ CDRom, Not ready
H:\ Fixed, Label: Windows Vista, Format: NTFS, Size: 32,570,863,616
// 是一个USB的Drive
I:\ Removable, Label: BOOKS, Format: FAT, Size: 1,041,989,632

特殊的文件夹

// 我的文档路径
Environment.GetFolderPath(Environment.SpecialFolder.Personal);

路径相关的函数

  • Path.IsPathRooted tells you if the path name begins with a drive or
    a backslash.
  • Path.HasExtension tells you if the filename has an extension.
  • Path.GetFileName returns just the filename part of the file path.
  • Path.GetFileNameWithoutExtension returns the filename without
    the extension.
  • Path.GetExtension returns just the filename extension.
  • Path.GetDirectoryName returns just the directory path of the file
    path.
  • Path.GetFullPath possibly prepends the current drive and directory
    to the file path.
  • Path.GetPathRoot obtains the initial drive or backslash (if any).
  • Path.Combine
  • Path.ChangeExtension

File、FileInfo和Directory、DirectoryInfo
本质上差不多,File类全是Static函数,而FileInfo需要创建Instance来调用,二者内容本质是一样的

这一大章节(25章)讲了很多文件读取、文件创建相关的操作,太无聊了,就不多说了,也不难,要用的时候再去查好了。



String Theory

C#里的String算是一个immutable的对象,不可以改变其长度,也不可以改变其里面单个的字符的值,只能Copy一个,然后去改复制的对象。这种情况下,会引发一个性能问题,当多次对一个String进行修改(比如加一个"__"的操作),会使得性能下降,因为此时不断的在进行String的Deep Copy,每次修改,都会让原本的string指向的堆上的内存,进入GC。

而使用StringBuilder,可以减少反复修改String带来的拷贝消耗,因为StringBuilder里是可以直接修改String的,而不是去修改复制后的对象,当String内存不够时,StringBuilder会在堆上重新分配内存(应该类似于C++的动态数组)。代码如下所示:

StringBuilder builder = new StringBuilder();
for (int i = 0; i < 10000; i++)
 builder.Append("abcdefghijklmnopqurstuvxyz\r\n");

string str = builder.ToString();// 使用ToString得到最终结果

除了使用StringBuilder,还可以使用上一章提到的StringWriter,性能也跟StringBuilder是差不多的,原理也差不多:

StringWriter writer = new StringWriter();
for (int i = 0; i < iterations; i++)
	writer.WriteLine("abcdefghijklmnopqurstuvxyz");

string str = writer.ToString();

PS:不过我要是把String的第5个字符,连续改10000次,不知道这些类可不可以用,如果按照动态数组理解,感觉应该是可以的



Generics

终于到书的最后一章了,C#2.0开始提出泛型,语法与C++模板类似。

举个例子吧,假如要设计一个2D的Point类型,它有X和Y两个Property,为了效率考虑,X和Y应该是整形,为了精度考虑,X和Y应该是Double类型,那么此时可能需要根据不同的需求设计不同的两个类,代码如下:

class IntegerPoint
{
	public int X;
	public int Y;
	public double DistanceTo(IntegerPoint pt)
	{
		return Math.Sqrt((X - pt.X) * (X - pt.X) + (Y - pt.Y) * (Y - pt.Y));
	}
}

class DoublePoint
{
	public double X;
	public double Y;
	public double DistanceTo(DoublePoint pt)
	{
		return Math.Sqrt((X - pt.X) * (X - pt.X) + (Y - pt.Y) * (Y - pt.Y));
	}
}

而利用泛型,就可以用T来代替double或者int,代码如下,与C++的写法很像:

// 这里的写法更简单, 不需要像C++一样写template<typename T>
// 这个T是惯用写法, 并不一定要写T, T is called a type parameter
class Point<T>
{
	public T X;
	public T Y;
	public double DistanceTo(Point<T> pt)// 返回类型都是double, 不需要改
	{
		// 注意, 这样写是错误的, 因为X类型未知, 不一定能相减, 后面会再说
		// 而且就算X类型能相减, Math.Sqrt接受的是double, X类型也不一定能转换成double类型
		return Math.Sqrt((X - pt.X) * (X - pt.X) + (Y - pt.Y) * (Y - pt.Y));
	}
}

// 使用
Point<int> pti = new Point<int>();
pti.X = 26;
pti.Y = 14;

Point<double> ptd = new Point<double>();
ptd.X = 13.25;
ptd.Y = 3E-1;

值得注意的是,这里的DistanceTo的泛型函数的写法是错误的,不过在C++里应该是正确的,因为C++模板的特性是单纯的生成函数而已,是在编译器确定的,如果不对会编译失败的。而C#这里的做法是使用了泛型里的一个概念:Constraints

Constrains对应的关键字是where,举几个例子:

// 使用在Point上的类型T, 必须继承于SomeBaseClass
class Point<T> where T: SomeBaseClass

// 使用在Point上的类型T, 必须是值类型
class Point<T> where T: struct

// 使用在Point上的类型T, 必须有一个无参的构造函数
class Point<T> where T: new()

// 使用,来写多个Contrains: 使用在Point上的类型T, 必须是引用类型, 且必须有无参的构造函数
class Point<T> where T: class, new()

然而,这些Contraints都不满足当下使用的DistanceTo函数的要求,这里的要求是:

  1. 类型T要可以相减
  2. 类型T可以转化为double类型

现实情况是,没有任何Contraints的写法,能让T支持the subtraction operator的。但是,却可以通过让T继承一个Interface的方法,来实现这个效果。

这里需要介绍一个System命名空间里的Interface,叫做IConvertible,继承于这个接口的Struct或Class需要满足一系列的往基本类型转换的函数,其中就包括ToDouble的转换函数,所以这里可以这么写:

using System;
using System.Globalization;
class Point<T> where T:IConvertible
{
	...

	NumberFormatInfo fmt = NumberFormatInfo.CurrentInfo;
	
	public double DistanceTo(Point<T> pt)
	{
		// 这里的fmt需要是一个继承了IFormatProvider接口的类的对象
		return Math.Sqrt(Math.Pow(X.ToDouble(fmt) - pt.X.ToDouble(fmt), 2) + Math.Pow(Y.ToDouble(fmt) - pt.Y.ToDouble(fmt), 2));
	}
}

顺便提一下IFormatProvider interface,它是System命名空间下的接口,用于进行格式转换的,在System.Globalization命名空间下提供了接口的三个派生类:

  • System.Globalization.CultureInfo:用于国家文化的格式转换
  • System.Globalization.DateTimeFormatInfo:用于日期的格式转换
  • System.Globalization.NumberFormatInfo:用于数字的格式转换

示例代码如下:

using System;
using System.Globalization;

public class Example
{
   public static void Main()
   {
      DateTime dateValue = new DateTime(2009, 6, 1, 16, 37, 0);
      CultureInfo[] cultures = { new CultureInfo("en-US"),
                                 new CultureInfo("fr-FR"),
                                 new CultureInfo("it-IT"),
                                 new CultureInfo("de-DE") };
      foreach (CultureInfo culture in cultures)
         Console.WriteLine("{0}: {1}", culture.Name, dateValue.ToString(culture));
   }
}
// The example displays the following output:
//       en-US: 6/1/2009 4:37:00 PM
//       fr-FR: 01/06/2009 16:37:00
//       it-IT: 01/06/2009 16.37.00
//       de-DE: 01.06.2009 16:37:00

完整的代码如下:

using System;
using System.Globalization;

class Point<T> where T : IConvertible
{
    public T X;
    public T Y;
    NumberFormatInfo fmt = NumberFormatInfo.CurrentInfo;
    
    // 注意不带参数的构造函数的写法, default operator会让值类型bit位为0, 让引用类型为null
    public Point()
    {
        X = default(T);
        Y = default(T);
    }

    // Two-Parameter Constructor
    public Point(T x, T y)
    {
        X = x;
        Y = y;
    }

    public double DistanceTo(Point<T> pt)
    {
        return Math.Sqrt(Math.Pow(X.ToDouble(fmt) - pt.X.ToDouble(fmt), 2) +
        Math.Pow(Y.ToDouble(fmt) - pt.Y.ToDouble(fmt), 2));
    }
}

下面是使用的代码:

class GenericPoints
{
    static void Main()
    {
        // Points based on integers
        Point<int> pti1 = new Point<int>();
        Point<int> pti2 = new Point<int>(5, 3);
        Console.WriteLine(pti1.DistanceTo(pti2));
        
        // Points based on doubles
        Point<double> ptd1 = new Point<double>(13.5, 15);
        Point<double> ptd2 = new Point<double>(3.54, 5E-1);
        Console.WriteLine(ptd2.DistanceTo(ptd1));
        
        // Points based on strings
        // 这是因为String类也继承了IConvertible接口, 里面的ToDouble函数调用了Double.Parse
        Point<string> pts1 = new Point<string>("34", "27");
        Point<string> pts2 = new Point<string>("0", "0");
        Console.WriteLine(pts1.DistanceTo(pts2));
		
		// 甚至还可以写Point<DateTime>...
    }
}

最后介绍了一些泛型容器,不多说:

Where generics had the biggest impact in the .NET Framework is with the System.Collections namespace. With .NET 2.0, that namespace has been largely superseded by the System.Collection.Generic namespace, which includes generic versions of Queue, Stack, Dictionary, SortedList, and List (which is the generic version of ArrayList). These versions provide type safety that the non-generic versions do not, and are now preferred for most applications.



Nullable Types

默认的,C#里的Class是可以为null,而Struct对应的值类型是不可以为null的。

但假设这里有这么个应用情况,想要一个函数,返回一个Vector3,如果计算错误,那么就返回一个null,代表没有返回结果。但是由于Vector3是值类型,直接返回null是会报错的。为了解决此类问题,.NET 2.0推出了Nullable Types,此类型的对象可以用null去对值类型赋值:

Any value type—int, bool, DateTime, or any structure that you define—can be made into a “nullable”

在.NET 2.0里,为了支持Nullablt Types,为C#提供了三个变化:

  1. 为System的命名空间里添加了A Nullable generic structure
  2. C# needed to recognize nullable types in some cases.
  3. CLR需要为了boxing,来识别nullable types

Nullable Types的实现
作者推测,底层的代码大概是这样的:

// T是一个值类型, Nullable也是个值类型, 其实就是一个wrapper, 包含了target和hasValue(用于标识是否为Null)
public struct Nullable<T> where T : struct // Pure supposition
{
    T value;
    bool hasValue;
    
    // 定义了一个带参数的Constructor, 但是要注意, Nullable类还有个值类型的默认构造函数
    public Nullable(T value)
    {
        this.value = value;
        hasValue = true;
    }
    // Read-Only Properties
    public bool HasValue
    {
        get { return hasValue; }
    }
    public T Value
    {
        get
        {
            if (!HasValue)
                throw new InvalidOperationException(
                "Nullable object must have a value");
            return value;
        }
    }
 ...
}

使用的时候这么写代码:

Nullable<DateTime> ndt = new Nullable<DateTime>();
// 抛出异常:Nullable object must have a value
Console.WriteLine(ndt);


Nullable<DateTime> ndt = new Nullable<DateTime>(DateTime.Now);
// 可以正常Print
Console.WriteLine(ndt);

// 如果想知道ndt是否为null, 需要通过ndt的hasValue来判断

Nullable Types与Value的相互转换
可再为类添加两个cast:

public struct Nullable<T> where T : struct // Pure supposition
{
	...
	// 提供value转向Nullable<T>的隐式转换
	public static implicit operator Nullable<T>(T value)
	{
		return new Nullable<T>(value);
	}
	// 提供Nullable<T>向value的显示转换, 此时的转换不支持隐式转换, 必须加上()转型的显示转换
	public static explicit operator T(Nullable<T> value)
	{
		return value.Value;
	}
	...
}

之所以两个转换,一个是隐式的,一个是显式的,是因为:

// 这里的Nullable<T>的集合范围是:所有T的值 + Null

// 从value隐式转换到Nullable<T>是OK的, 因为右边不可能为Null,
ndt = DateTime.Now;

// 从Nullable<T>隐式转换到是不OK的, 因为右边可能为Null, 此时转换后的值没有意义
DateTime dt = ndt; // Won’t work!

// 所以必须显示转换, 如果ndt的hasvalue为false, 那么会抛出异常
DateTime dt = (DateTime) ndt;

GetValueOrDefault
前面的GetValue函数,如果value的hasValue为false,读取value的时候就会抛出异常,而GetValueOrDefault可以避免抛出异常,而是返回默认构造函数产生的值类型,它还有重载函数,可以指定默认值:

DateTime dt = ndt.GetValueOrDefault(new DateTime(1900, 1, 1));

其内部实现的代码,可能是这样的:

public struct Nullable<T> where T : struct // Pure supposition
{
	...
	public T GetValueOrDefault()
	{
		return HasValue ? Value : new T();	
	}
 
 	public T GetValueOrDefault(T defaultValue)
	{
		return HasValue ? Value : defaultValue;
	}
	...
}

实现Nullablt Type剩下的接口
就是Object里剩余要实现的接口:ToString、GetHashCode和Equals函数,本质跟值类型差不多,无非是多了Nullable情况下的特殊处理:

public struct Nullable<T> where T : struct // Pure supposition
{
	...
	// 实现ToString函数
	public override string ToString()
	{
		return HasValue ? Value.ToString() : "";
	}
	public override int GetHashCode()
	{
		return HasValue ? Value.GetHashCode() : 0;
	}
	
	// 
	public override bool Equals(object obj)
	{
		if (obj.GetType() != GetType())
			return false;
		Nullable<T> nt = (Nullable<T>)obj;
		if (nt.HasValue != HasValue)
			return false;
		
		return HasValue && Value.Equals(nt.Value);
	} 
	...
}

最后一步 ——— 让null可以用于Nullable类型
这里的Nullable仍然是值类型,是不可以用null来赋值的,此时就轮到C#的编译器来简化这个工作了,此时如果写这么一行代码:

Nullable<DateTime> ndt = null;

C#编译器会将其变为下面代码对应的CIL:

Nullable<DateTime> ndt = new Nullable<DateTime>();// 默认构造函数, hasValue为false

C#还把下面这行代码:

ndt == null

替换成:

!ndt.HasValue
// ndt != null也是同理的

C#还把Nullable Type的声明语法做了简化:

Nullable<DateTime> ndt;
// 简化为
DateTime? ndt;

bool? nb = null;// 等同于Nullable<bool> ndt;

bool? nb = true;// 等同于Nullable<bool> ndt = new Nullable<bool>(true);

// 使用的时候, nb向bool仍然需要显式转换
if (nb != null)
{
	if ((bool)nb)// 必须显式转换
	{
		... // true case
	}
	else
	{
		... // false case
	}
}

最后几种写法:

// 使用Nullable Type为值类型赋值, ??叫做null coalescing operator, 当ndt为null时, dt为后者
DateTime dt = ndt ?? new DateTime(2007, 1, 1);

Nullable类
除了Nullable泛型类,C#还提供了Nullable类,这是个静态类,可以来:

  • use to compare two objects based on nullable types
  • it also has a static method named GetUnderlyingType that you can use in connection with reflection.

CLR需要为了boxing,来识别nullable types
前面的操作只是单独设计了个泛型类,然后做了一些编译器的简化工作,并没有改变CLR,但是这会出现这么一种情况:

// 创建一个可以为null的int
int? ni;
// 把它装箱, 得到obj
object obj = ni;// 由于ni是一个Nullable<int>类型的值类型对象, 此时的obj不为null

此时的obj竟然不是null,那么肯定是不正确的,为了解决这个问题,所以需要改变CLR,CLR需要为了boxing,来识别nullable types,在其装箱时,如果它的hasValue为false,CLR会强行改它为null



Appendix

构造函数的初始化顺序问题

// 成功
ObjectField objField = new ObjectField
{
    objectType = typeof(GameObject),
    value = go,
};

// Runtime报错
ObjectField objField = new ObjectField
{
    objectType = typeof(GameObject),
    value = go,
};


partial method

partial class已经很清楚了,比如我有个类MyClass,有一部分是Editor下才执行的,那么我可以这么写:

// MyClass.cs文件
partial class MyClass
{
	void MyFunc()
	{
		EditorFunc();
	}
}

// MyClass.Editor.cs文件
partial class MyClass
{
#if UNITY_EDITOR
    void EditorFunc()
    {
    	...
	}
#endif
}

如果是这样的话,在Runtime下编译会报错,因为EditorFunc不可以在Runtime执行,此时可以改成这样:

void MyFunc()
{
#if UNITY_EDITOR
	EditorFunc();
#endif
}

这样的缺点是要去改动原本Runtime的脚本,这里介绍一个partial method的方法,因为partial method可以实现函数的声明和定义分离,代码如下:

// MyClass.Editor.cs文件
partial class MyClass
{
	partial void EditorFunc();
#if UNITY_EDITOR
    partial void EditorFunc()
    {
    	...
	}
#endif
}


C#的静态method参数里出现的this关键字

参考链接:https://stackoverflow.com/questions/846766/use-of-this-keyword-in-formal-parameters-for-static-methods-in-c-sharp
代码如下:

public static int Foo(this MyClass arg)

这种写法,叫做Extension Method,这玩意儿感觉是C#的奇淫技巧,它可以为一个CLR里的类型,在不改变该类代码、也不重新编译原本类的情况下,为其添加新的method.

举个例子,有个String,现在想判断它是不是Email格式的,那么我们可能会这么写:

string email = GetARrandomString();

// 这里设计一个类, 提供了IsValid的静态方法
if (EmailValidator.IsValid(email) ) {
	...
}

而有了Extension Method,就可以直接这样写:

string email = GetARrandomString();
if (email.IsValidEmailAddress()) {
 	...  
}

而实现上面功能的核心代码,就是这样:

public static class ScottGuExtensions
{
	// 核心就是这个this关键字, 它表示是通过string s本身去调用这个函数
    public static bool IsValidEmailAddress(this string s)
    {
        Regex regex = new Regex(@"^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$");
        return regex.IsMatch(s);
    }
}

然后别忘了,用的时候加上using ScottGuExtensions或者相关前缀就行了。

总的来说就是个Syntax Sugar,也没啥大不了的


Activator.CreateInstance与new的区别

参考资料:https://stackoverflow.com/questions/1649066/activator-createinstancet-vs-new
https://docs.microsoft.com/en-us/dotnet/api/system.activator.createinstance?redirectedfrom=MSDN&view=net-5.0#System_Activator_CreateInstance__1

比如说这两行代码有什么区别:

Student s1 = Activator.CreateInstance<Student>();
Student s1 = new Student();

Activator.CreateInstance方法是用于创建泛型对象的,比如说有这么一个静态函数:

// 类型T必须有一个无参的构造函数
public static T Factory<T>() where T: new()
{
    return new T();
}

这段执行代码里的new T(),T的类型其实是在编译期决定的,编译器会把它转换为调用CallInstance的方法。比如说,有个类型,是用var获得的,那么它可以通过CreateInstance来创建该类型的对象,但是不可以用New来创建,示例如下:

var viewType = Random.GetType();
var baseNodeView = Activator.CreateInstance(viewType);// 创建该类型的对象
var baseNodeView = new viewType();// 错误
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值