获取对象的字符串表示 ToStirng
System.Object定义了一个pulic,virtual,无参的ToString方法.
System.Object的ToStirng实现的只是返回对象所属类型的全名,这对于许多不能提供有意义的字符串的类型来说,这是一个合理的默认值.
想要提供合理的方式获取对象当前值的字符串表示,就应重写ToString方法. 定义类时应该总是重写ToString方法,以提供良好的调试支持.
指定具体的格式和语言文化
无参的ToString方法有两个问题:
- 无法控制字符串的格式
- 不能选择一种特定的语言文化来格式化字符串
为了解决这个,类型应该实现System.IFormattable
接口:
// FCL的所有基类型都实现了这个接口, 另一些类型(Guid)也实现了它
public interface IFormattable
{
String ToString(String format, IFormatProvider formatprovider);
}
IFormattable方法获取两个参数,
String format
: 告诉方法如何格式化字符串formatprovider
: 是一个实现了IFormatProvider接口的实例类型,提供具体的语言文化信息.
FCL中定义的许多类型都能同时识别几种格式, 例如,
- DateTime
- d 表示短日期
- D 表示长日期
- g 表示常规
- M 表示月/日
- s 表示可排序
- T 表示长时间
- u 表示ISO8601格式的协调世界时
- U 表示长日期格式的协调世界时
- Y 表示年/月
- 所有枚举
- G 表示常规
- F 表示标志
- D 表示十进制
- X 表示十六进制
- 所有内建数值类型
- C 表示货币格式
- D 表示十进制格式
- E 表示科学计数法
- F 表示定点(fix-point)格式
- G 表示常规格式
- N 表示数字格式
- P 表示百分比格式
- R 表示往返行程(round-trip)格式
- 保证转换为字符串的数值再次被分析为相同的数值
- 此格式仅有浮点型(Single,Double)支持
- X 表示十六进制
- 数值类型还支持 picture数值格式字符串###,###可以显示千分位分隔符. (详情参考自定义数字格式字符串)
大多数类型,调用ToStirng传递null完全等价于传递格式字符串G
.
关于语言文化,默认无参ToString方法默认使用与调用线程关联的语言文化信息进行格式化.如果formatprovider传null,IFormattable的ToString也这么做.
格式化数组(货币,整数,浮点数,百分比,日期和时间)适合应用对语言文化敏感的信息.
// 一下代码将以越南地区适用的货币格式来获取一个Decimal数值的字符串表示.
Decimal price = 123.54M;
String s = price.ToStirng("C",new CultureInfo("vi-VN"));
MessageBox.Show(s);
// 这个版本调用ToString(null,null);
// 采用常规数值格式,采用线程的语言文化信息
public override String ToString();
// 是ToString的真正实现
// 采用由调用者指定的格式和语言文化信息
public String ToString(String format, IFormatProvider formatprovider);
// 简单的调用ToString(null,formatprovider);
// 实现了IConvertible的ToString方法
public String ToString(IFormatProvider formatprovider);
将多个对象格式化成一个字符串
String s = String.Format("On {0:D}, {1} is {2:E} years old.", new DateTime(2019, 8, 15), "AA", 9);
// 输出: On 2019年8月15日, AA is 9.000000E+000 years old.
Console.WriteLine(s);
在Format中,解析字符串时, 发现可替换参数0
应该调用它的IFormattable接口的ToString方法,并为方法传递”D”,null参数.
但是如果使用StringBulider而不是String来构造字符串,可以调用StringBulider的AppendFormat方法.AppendFormat原理与String的Format相似.
Console的Write/WriteLine要格式化符合特定语言文化的字符串必须要调用String的Format方法.
提供定制格式化器
可以定义一个方法,在任何对象需要格式化字符串的时候由StringBuilder
的AppendFormat
方法调用该方法,按照我们希望的任何方式格式化对象. (也适用于String的Format方法)
也就是说,AppendFormat
不是为每个对象调用ToString方法,而是调用自定义的方法.
以下代码为了将所有的Int32值在HTML中加粗显示.
/// <summary>
/// 格式化定制器
/// 将所有的Int32值在HTML中加粗显示.用<B></B>加粗显示
/// </summary>
namespace FormatController
{
class Program
{
public static void Main(string[] args)
{
StringBuilder sb = new StringBuilder();
// 传入定制类BoldInt32s为参数
sb.AppendFormat(new BoldInt32s(), "{0} {1} {2:M}", "AAA", 23, DateTime.Now);
//输出 AAA <b>23</b> 8月15日
Console.WriteLine(sb);
Console.ReadKey(true);
}
}
// 需要实现IFormatProvider和ICustomFormatter接口
internal sealed class BoldInt32s : IFormatProvider, ICustomFormatter
{
public object GetFormat(Type formatType)
{
if(formatType == typeof(ICustomFormatter))
{
return this;
}
return Thread.CurrentThread.CurrentCulture.GetFormat(formatType);
}
public string Format(string format, object arg, IFormatProvider formatProvider)
{
string s;
IFormattable formattable = arg as IFormattable;
if(formattable == null)
{
s = arg.ToString();
}
else
{
s = formattable.ToString(format, formatProvider);
}
if(arg.GetType() == typeof(Int32))
{
return "<b>" + s + "</b>";
}
return s;
}
}
}
AppendFormat的工作方式:
- 需要格式化一个可替换参数时,会调用ICustomFormatter的Format方法
- 如果不支持就调用简单的无参的ToString方法
- 如果对象支持IFormattable就调用支持富格式化的ToString,向它传递字符串和格式提供器.
- 最后核实类型是否是需求的类型,处理自定义操作.
解析字符串来获取对象 : Parse
从某种意义上来说,Parse扮演了一个工厂的角色. 能解析字符串的任何类型都提供了公共静态方法Parse
.
以Int32类型的Parse方法为例(其他数值类型的Parse方法与此相似):
public static Int32 Parse(String s,NumberStyles style, IFormatProvider provider);
NumberStyles
参数是标志集合,标识了Parse应在字符串查找的字符,也就是s中允许的样式,不允许的会抛出异常.(也就是说 string中不能出现不对应类型的字符)IFormatProvider
获取语言文化特有的信息
```csharp
// 会抛出FormatException
// 字符串包含了前导空白符
// Int32 x = Int32.Parse(“ 123”,NumberStyles.None, null);
// 这样修改,能跳过前导空白符
Int32 x = Int32.Parse(“ 123”,NumberStyles.AllowLeadingWhite, null);
// 解析16进制数
Int32 x = Int32.Parse(“1A”,NumberStyles.HexNumber, null); // x : 26
// 上述方法需要传递3个参数,为了简化编程,还提供了Parse方法的4个重载版本.
[数字NumberStyles的详细文档.](https://docs.microsoft.com/zh-cn/dotnet/api/system.globalization.numberstyles?redirectedfrom=MSDN&view=netframework-4.8)
[日期的DateTimeStyles的详细文档](https://docs.microsoft.com/zh-cn/dotnet/api/system.globalization.datetimestyles?redirectedfrom=MSDN&view=netframework-4.8)
对日期和事件的解析比较复杂, DateTime类型的Parse方法过于宽松,还提供`ParseExact`方法,接收一个picture参数,能准确描述应该如何格式化日期/时间字符串,以及如何解析.
[参考DateTimeFormatInfo的详细文档](https://docs.microsoft.com/zh-cn/dotnet/api/system.globalization.datetimeformatinfo.-ctor?redirectedfrom=MSDN&view=netframework-4.7.2)
## 关于Parse性能上的问题
如果应用程序频繁调用Parse,而且Parse频繁抛出异常(用户无效输入),应用程序性能会显著下降.
为此, Microsoft为所有的`数值数据类型`,`DateTime类型`,`TimeSpan类型`,`IPAddress类型`中加入`TryParse`方法.
```csharp
// 方法返回bool, 指出传递的字符串是否能解析成功,以传引用的方式传给result
public static bool TryParse(string s,NumberStyles style,IFormatProvider provider,
out int result);
编码: 字符和字节的相互转换
用System.IO.BinaryWriter
或者System.IO.StreamWriter
类型将字符串发送给文件或网络流时,通常要进行编码。对应地,System.IO.BinaryReader
或者System.IO.StreamReader
类型从文件或网络流中读取字符串时,通常要进行解码。不显式指定一种编码方案,所有这些类型都默认使用UTF-8。
FCL提供了一些类型来简化字符编码和解码. 两种最常用的编码方案是UTF-16和UTF-8.
UTF-16
: 将每个16位字符编码成2个字节。不会对字符产生任何影响,也不发生压缩—性能非常出色。UTF-16编码也称为Unicode编码。(可以从低位优先
转换成高位优先
,或者高位优先
转换成低位优先
)UTF-8
: 将部分字符编码成1个字节,部分编码成2个字节,部分编码成3个字节或4个字节。值在0x0080之下的字符压缩成1个字节,适合表示美国使用字符。0x00800~0x07FF的字符转换成2个字节,适合欧洲和中东语言。0x0800以及之上的字符转换成3个字节,适合东亚语言。最后代理项对表示成4个字节。0x0080编码方法非常流行,但如果要编码的许多字符都具有0x0800或者之上的值,效率不如UTF-16ASCII
: 编码方案将16位字符编码从ASCII字符:也就是说,值小于0x0080的16位字符被转换成单字节。值超过0x007F的任何字符都不能被转换,否则字符的值会丢失。
应该总是选择UTF-16 UTF-8编码
String s = "Hi there.";
// 它知道怎么使用UTF-8来进行编码和解码
Encoding encodingUTF8 = Encoding.UTF8;
// 将字符串编码成字节数组
Byte[] encodingBytes = encodingUTF8.GetBytes(s);
// 显式编译好的字节值
// 输出:48-69-20-74-68-65-72-65-2E
Console.WriteLine(BitConverter.ToString(encodingBytes));
// 将字节数组解码回字符串
String decodeString = encodingUTF8.GetString(encodingBytes);
// 显式解码的字符串
// 输出: Hi there.
Console.WriteLine(decodeString);
不建议使用Encoding类的静态属性Defult,它返回的是对象使用用户当前的代码页来进行编码和解码(在用户控制面板的区域和语言选项中指定),也就有是说应用程序的行为会随着机器的设置而变.
UnicodeEncoding
,UTF8Encoding
,UTF32Encoding
,UTF7Encoding
提供了多个构造器, 允许对编码和前导码进行更多的控制. 前3个类还提供了特殊的构造器,允许在对一个无效的字节序列进行解码时抛出异常,应该使用这些能抛出异常的类.
前导码: 也称字节顺序标记
(BOM,Byte Order Mark).
字符和字节流的编码和解码
字节流通常以数据块data chunk的形式传输, 可能先从流中读5个字节,然后再7个字节,因为UTF-16每个字符都是2个字节,那么调用Encoding的GetString方法传递一个5字节的数组返回的字符串只包含2个字符,后面的7个字节返回3个,显然会造成数据损坏.
字节块解码首先要获取一个Encoding对象,在调用其GetDecoder
方法.
- 返回一个新构造对象的引用,从抽象类
System.Text.Decoder
派生. - Decoder提供了2个重要方法
- GetChars和GetCharCount.
- 它会尽可能多的解码字节数组. 如果剩余一个字节,
- 则会保存到Decoder对象内部,下次调用时取出,和新的字节数组合并.
- 这样就能正确解码.
从流中读取字节时,Decoder对象的作用很大.
每次调用从Encoder派生的对象都会维护余下数据的状态信息,以便成块的方式对数据进行编码.
Base-64字符串编码和解码
除了UTF-16和UTF-8,另一个流行方案是将字节序列
编码成Base-64字符串
.
例外的是,Base-64编码和解码不是用Encoding派生类型来完成,而是用Convert
的静态方法ToBase64String或ToBase64CharArray方法
.
// 获取10个随机生成的字节
Byte[] bytes = new Byte[10];
new Random().NextBytes(bytes);
// 将字节解码成Base-64字节串,并显示字符串
String s = Convert.ToBase64String(bytes);
// 输出: usLVdVlIgAgfsw==
Console.WriteLine(s);
// 将Base-64字符串编码回字节,并显示
bytes = Convert.FromBase64String(s);
// 错误用法, 显示的是类名 System.Byte[]
// Console.WriteLine(bytes.ToString());
// 输出: BA-C2-D5-75-59-48-80-08-1F-B3
Console.WriteLine(BitConverter.ToString(bytes));
安全字符串
String对象可能包含敏感数据,比如信用卡资料,密码. String对象会在内存中包含一个字符数组. 执行不安全代码或者非托管代码就可以扫描进程的地址空间,找到包含敏感数据的字符串.
即使String对象只用一小段时间就进行垃圾回收, CLR也可能无法立即重用String对象的内存, 致使String的字符长时间保留在进程的内存中. 由于字符串不可变,处理它们的时候,旧的副本会逗留在内存中,最终造成多个不同版本的字符串散布在整个内存空间中.
FCL增加了一个更安全的字符串类:System.Security.SecureString
构造SecureString
对象时,会在内部分配一个非托管内存块,其中包含一个字符数组.
- 使用非托管内存块是为了避开垃圾回收器的魔爪.
- 这些字符串是经过加密的,能防范任何恶意的非安全/非托管代码获取机密信息.
SecureString
类的操作方法:
AppendChar
附加InsertAt
插入RemoveAt
删除SetAt
设置一个字符
但是调用这些方法时,方法内部会进行 解密 ,执行完指定的操作再 重新加密 字符串.这说明有一段时间是处于未加密的状态. 并且这些操作性能会比较一般.
SecureString
类实现了IDisposable
接口. 用简单的方式 确定性 地摧毁字符串中的安全内容. 只需要调用SecureString
的Dispose
方法. 在方法内部,Dispose会对内存缓冲区的内容进行清零.然后释放缓冲区.
SecureString对象内部有一个SafeBuffer派生的对象负责维护字符串,SafeBuffer类最终从CriticalFinalizerObject类派生,所以在垃圾回收时,内容保证清零,缓冲区被释放.
SecureString
对象被垃圾回收后,加密的字符串的内容将不再存于内存中.
FCL限制了对SecureString类
的支持. 只有少数方法才能接受SecureString参数
.
要使用的话可以创建自己的方法来接收SecureString对象参数,方法内部必须先让SecureString对象创建一个非托管内存缓冲区,它将用于包含解密过的字符,然后才能让该方法使用缓冲区.为了最大程度降低恶意代码获取敏感数据的风险,你的代码在访问解密过的字符串时,时间应尽可能短,结束使用后,代码尽快清零并释放缓冲区. 不要将SecureString内容放到一个String中. String会在堆中保持未加密的状态. 并且SecureString没有重写ToString方法,就是为了避免这个.
public static void Main(string[] args)
{
// using代码块之后,SecureString被dispose,内存中无敏感数据
using (SecureString ss = new SecureString())
{
Console.WriteLine("请输入密码:");
while (true)
{
// 参数true意思是,拦截输入,不显示在控制台上
ConsoleKeyInfo cki = Console.ReadKey(true);
// 回车表示输入完成
if (cki.Key == ConsoleKey.Enter) break;
// 将密码附加到SecureString中
ss.AppendChar(cki.KeyChar);
Console.Write("*");
}
Console.WriteLine();
// 密码已经输入,出于演示的目的显示它
DisplaySecureString(ss);
}
}
// 这个代码是unsafe,要访问非托管内存
private unsafe static void DisplaySecureString(SecureString ss)
{
Char* pc = null;
try
{
// 将SecureString解密到一个非托管内存缓冲区
pc = (char*) Marshal.SecureStringToCoTaskMemUnicode(ss);
// 访问包含已经解密SecureString字符的非托管内存缓冲区
for (int i = 0; pc[i] != 0; i++)
{
Console.Write(pc[i]);
}
}
finally
{
// 确定清零并释放包含已解锁SecureString字符的非托管内存缓冲区
if (pc != null)
{
Marshal.ZeroFreeCoTaskMemUnicode((IntPtr) pc);
}
}
}
什么情况下用SecureString?
如果以下情况下,SecureString
非常有用:
- 您可以逐字符构建它(例如从控制台输入),或者从非托管API获得它。
- 您可以通过将它传递给非托管API(SecureStringToBSTR)来使用它。
如果您曾经将其转换为托管字符串,则您已经放弃了它的用途。
我会看到的主要用例是在客户端应用程序中,它要求用户输入高度敏感的代码或密码。用户输入可以逐字符用于构建SecureString,然后将其传递给非托管API,该API对它使用后接收的BSTR进行零。任何后续内存转储都不会包含敏感字符串。
在服务器应用程序中,很难看出它在哪里有用。
Marshal类提供用于操纵安全字符串的方法
System.Runtime.InteropServices.Marshal类
提供5个方法来将一个SecureString
的字符 解密到非托管 内存缓冲区. 所有方法都是静态方法.所有方法都接受一个SecureString参数,并返回IntPtr.
每个方法都有一个配对方法来清零并释放内部缓冲区.