一,从时间(DateTime)出发
先上一段处理时间格式化的代码。该代码在Net6.0框架下运行。
var time = new DateTime(2023, 8, 24); Console.WriteLine(time); Console.WriteLine(string.Format("{0:yyyyMMdd}", time));//写法1 Console.WriteLine("Time is {0:yyyyMMdd}", time);//写法2 Console.WriteLine(time.ToString("yyyyMMdd"));//写法3 //2023 / 8 / 24 0:00:00 //20230824 //Time is 20230824 //20230824
以上代码通过不同的方式将时间格式化,在“yyyyMMdd”的格式要求下,都输出了同样的“20230824”。在DateTime格式化的时候又是怎样识别到“yyyyMMdd”并返回对应的结果?
二,初探源码
public static string Format(string format, object? arg0) { return FormatHelper(null, format, new ParamsArray(arg0)); }Format
private static string FormatHelper(IFormatProvider? provider, string format, ParamsArray args) { if (format == null) throw new ArgumentNullException(nameof(format)); var sb = new ValueStringBuilder(stackalloc char[256]); sb.EnsureCapacity(format.Length + args.Length * 8); sb.AppendFormatHelper(provider, format, args); return sb.ToString(); }FormatHelper
通过string.Fomat的源代码往上找,可以看到实际调用的ValueStringBuilder的AppendFormatHelper方法后,后返回ToString()。分别查找AppendFormatHelper和ToString的实现做了什么。
1 internal void AppendFormatHelper(IFormatProvider? provider, string format, ReadOnlySpan<object?> args) 2 { 3 4 //省略部分代码... 5 6 // Query the provider (if one was supplied) for an ICustomFormatter. If there is one, 7 // it needs to be used to transform all arguments. 8 ICustomFormatter? cf = (ICustomFormatter?)provider?.GetFormat(typeof(ICustomFormatter)); 9 10 // Repeatedly find the next hole and process it. 11 int pos = 0; 12 char ch; 13 while (true) 14 { 15 16 ReadOnlySpan<char> itemFormatSpan = default; // used if itemFormat is null 17 18 string? s = null; 19 string? itemFormat = null; 20 21 22 if (cf != null) 23 { 24 if (!itemFormatSpan.IsEmpty) 25 { 26 itemFormat = new string(itemFormatSpan); 27 } 28 29 s = cf.Format(itemFormat, arg, provider); 30 } 31 32 if (s == null) 33 { 34 // Otherwise, fallback to trying IFormattable or calling ToString. 35 if (arg is IFormattable formattableArg) 36 { 37 if (itemFormatSpan.Length != 0) 38 { 39 itemFormat ??= new string(itemFormatSpan); 40 } 41 s = formattableArg.ToString(itemFormat, provider); 42 } 43 else 44 { 45 s = arg?.ToString(); 46 } 47 48 s ??= string.Empty; 49 } 50 51 //省略部分代码... 52 } 53 }AppendFormatHelper
可以看到如果提供IFormatProvider,则可以通过自定义的ICustomFormatter.Format获取对应的格式。而如果参数继承了IFormattable,则调用IFormattable.ToString(string? format, IFormatProvider? formatProvider)方法。
public readonly partial struct DateTime : IComparable, ISpanFormattable, IConvertible, IComparable<DateTime>, IEquatable<DateTime>, ISerializable public string ToString(string? format) { return DateTimeFormat.Format(this, format, null); } public string ToString(string? format, IFormatProvider? provider) { return DateTimeFormat.Format(this, format, provider); }DateTime
其实’string.Format‘(写法1)和 ’time.ToString("yyyyMMdd")‘(写法2)都是调用到DateTimeFormat.Format(this, format, null);
而关于 DateTimeFormat.Format 的具体实现这里不详细说明,感兴趣者可点击链接跳转源码自行研究。而本文着重于IFormatProvider与IFormattable。
DateTime实现了接口ISpanFormattable,该接口也继承自IFormattable。当DateTime需要进行字符串格式化时,会调用到IFormattable.ToString(string? format, IFormatProvider? formatProvider)。format传入格式,而IFormatProvider可以对不同的参数类型
提供不同的自定义的ICustomFormatter对格式进行不同的处理。
public interface IFormattable { string ToString(string? format, IFormatProvider? formatProvider); } public interface IFormatProvider { object? GetFormat(Type? formatType); } public interface ICustomFormatter { string Format(string? format, object? arg, IFormatProvider? formatProvider); }IFormattable,IFormatProvider,ICustomFormatter
三,拓展
那么,如果想对某个类做到类似于time.ToString("yyyyMMdd")同等的效果,该如何改造呢?先查看以下例子。
void Test() { //example var p = new Person(); p.Name = "路人甲"; //输出Name Console.WriteLine("{0:N}", p); Console.WriteLine(p.ToString("N")); } internal class Person { public string Name { get; set; } public int Age { get; set; } public DateTime Birthdate { get; set; } }
Person类有属性Name。当Person实例进行字符串格式化输出,以“N”为格式,输出Name属性。当然编译器不会让上述代码通过编译,因为会报出错误CS1501:“ToString”方法没有采用 1 个参数的重载
那么可以分两种情况进行修改,
1,有修改Person类的权限,可以重新编译Person类所在的项目并生成新的程序集
1)让Person类继承IFormattable并实现其方法。
internal class Person:IFormattable { public string Name { get; set; } public string ToString(string? format, IFormatProvider? formatProvider=null) { switch (format) { case "N": return Name; default: return this.ToString(); } } }
2,没有修改Person类的权限
1)通过扩展方法实现类似于time.ToString("yyyyMMdd")同等的效果
public static class PersonExtenion { public static string ToString(this Person person2, string? format, IFormatProvider? formatProvider) { if (formatProvider == null) { switch (format) { case "N": return person2.Name; default: return person2.ToString(); } } else { var custom = formatProvider.GetFormat(person2.GetType()); return ((ICustomFormatter)custom).Format(format, person2, formatProvider); } } }PersonExtenion
2)在调用string.Format时传入自定义的IFormatProvider和ICustomFormatter
internal class PersonFormatProvider : IFormatProvider { public object? GetFormat(Type? formatType) { return new PersonDefaultFormatter<Person>(); } } public class PersonDefaultFormatter<T> : ICustomFormatter where T: Person { public string Format(string? format, object? arg, IFormatProvider? formatProvider) { switch (format) { case "N": return ((T)arg).Name; default: return arg.ToString(); } }PersonFormatProvider,PersonDefaultFormatter
当然,上述代码依然有瑕疵。当传入多个相同类型或不同参数时(如下),因为同一字符串共用同一个IFormatProvider,则传入args的类型作多次判断分开处理。
Console.WriteLine(string.Format(new PersonFormatProvider(), "{0:N}'s birthday is {1:yyyyMMdd}", person,person.Birthdate ));
此时建议选择StringBuilder实例的appendFomat或append方法将字符串分开处理。
标签:IFormattable,string,format,C#,Format,IFormatProvider,ToString,public From: https://www.cnblogs.com/mikodopants/p/17654540.html