string.Format、IFormattable、IFormatProvider、ICustFormatter

学到LINQ碰到 IFormattable 看不懂,刚好string.Format 也不是很懂,这里一起学习

参考文章:点这里

 

string.Format 方法时 string 类提供的静态方法,一般最多使用的时两个参数的重载。

var name = "ZhangSan";
var msg = string.Format($"Hello Cnblogs, I am {name}, Today is {DateTime.Now} {DateTime.Now.DayOfWeek}.");
//即var msg = string.Format($"Hello Cnblogs, I am {0}, Today is {1:yyyy-MM-dd}
{2}.",name,DateTime.Now,DateTime.Now.DayOfWeek);
Console.WriteLine(msg);

 

这边用了 $ 符号,算是一个语法糖,编译结果是一样的,正常来说后面的参数时是一个数组,就是两个参数的方法重载

当然,直接用加法相加也是可以得到相同结果

var msg1 = "Hello Cnblogs, I am " + name + ",Today is " + DateTime.Now.ToString("yyyy-MM-dd") + " " + DateTime.Now.DayOfWeek + ".";

 

一般用第一种方法是可读性更好,性能暂时还不了解

 

第一种方法的实现原理

1、Format 方法的内部解析方式和原理

Format方法在取到第一个参数时,会把其分成多份,每到个{ }就是一个新的,+ 号也是。需要用到{ }为字符串时,需要转义 }}、{{ 打两个就是原来字符串的意思了。

在对{ }中的序号对应第二个参数时,参数有多,无影响,参数不够,运行报错(编译不会报错)

序号可以是无序的,与第二个索引一至即可

这一步后就是使用StringBuilder的Append方法将各个部分添加进去,最后再用ToString方法转成string,实现原理就是每个部分都用一个Append方法加进去。

string和stringBuilder:string虽然是引用类型,但是该类型 .net内部进行了特殊处理,让其表现出和值类型相似的特征,特别是每次变动之后就会重新分配内存空间,而StringBuilder就不会,所以如果有很多个字符串相加拼接,则string性能较低。

在使用Append方法添加的时候会有两种情况:

一种是{0},{1}这样不带有特殊格式化的则直接会调用该对象的ToString方法,比如 str.Append(DateTime.Noe.DayOfWeek);其实就是str.Append(DateTime.Noe.DayOfWeek.ToString());在 .net中,如果是自己定义的类,没有重写自己的ToString方法,就会输出类的全名。

另一种是{0: yyyy-MM-dd}带有特殊格式化的则继续分解,将冒号后面的内容分解出来,并且在调用ToString时作为参数传入,如str.Append(DateTime.Noe.DayOfWeek.ToString("yyyy-MM-dd"));就体现了这一点。所以说这个冒号就是一个预定义好的标记而已。

 

2、ToString方法深入理解

ToString方法不重写是输出类的全名,因为只要是一个对象,它就是Object的子类,那么它就继承了Object中的ToString方法。在输出使用时直接上对象的话是直接默认调用ToString方法的,所以一般最后自己带上。

就是要记得自己重写ToString方法,以防出现输出类的全名的情况。

 

3、ToString带有自定义格式化参数的理解

ToString内部正常处理{0: xxx}是直接按照{0}处理的,其真正调用方法签名时 string ToString(string format, IFormatProvider foematProvider) ,而且也不是直接调用对象的此方法。而是通过IFormattable接口实现的方法,需要的时候应当实现IFormattable这个接口,接下来让我们用几个例子来一点一点剖析它

    class PersonWithToString1
    {
        public string Name { get; set; }
        public PersonWithToString1(string name)
        {
            this.Name = name;
        }

        public override string ToString()
        {
            return Name;
        }

        public string ToString(string format)
        {
            switch (format)
            {
                case "UPP":
                    return Name.ToUpper();
                case "LOW":
                    return Name.ToLower();
                default:
                    return Name;
            }
        }
    }
PersonWithToString1
    PersonWithToString1 person = new PersonWithToString1("ZhangSan");
    var msg = string.Format($"Hello,I'm {person.ToString("UPP") },Today is {DateTime.Now:yyyy-MM-dd}");
    Console.WriteLine(msg);

    msg= string.Format($"Hello,I'm {person.ToString():UPP},Today is {DateTime.Now:yyyy-MM-dd}");
    Console.WriteLine(msg);

 

 

 从以上结果来看,验证了上面说的ToString内部正常处理{0: UPP}是直接按照{0}处理的

    class PersonWithToString2:IFormattable
    {
        public string Name { get; set; }
        public PersonWithToString2(string name)
        {
            this.Name = name;
        }

        public override string ToString()
        {
            return Name;
        }

        public string ToString(string format,IFormatProvider formatProvider)
        {
            if (string.IsNullOrEmpty(format))
                return ToString();

            switch (format)
            {
                case "UPP":
                    return Name.ToUpper();
                case "LOW":
                    return Name.ToLower();
                default:
                    return Name;
            }
        }
    }
PersonWithToString2
    PersonWithToString2 person2 = new PersonWithToString2("ZhangSan");
    var msg = string.Format($"Hello,I'm {person2:UPP},Today is Good Day!");
    Console.WriteLine(msg);

 

 

 从以上结果可知{0:UPP}会调用接口定义的ToString方法,那么{0}呢,没有实现IFormattable接口,会调用重载的或者是基类的ToString方法。如果实现了IFormattable就是直接调用接口定义的ToString方法了。

总结:

一.对于实现IFormattable 接口时,如果format参数为null(即不带格式化参数的情况,如{0})则应该调用重载的 ToString()方法,而不应该自己去另外写代码,直接自行跳转到 .ToString()。

二.如果找不到相应的格式化参数,例如{0:AAA},在Person2的switch中并无匹配的AAA,这种情况一般也应该去调用重载的 ToString()方法

 

4、继续了解IFormatProvider 和 ICustomFormatter 接口

到这里为止,可以说的确可以灵活应用string.Format() 方法了,但还是存在一些问题,比如我们必须得为每个类单独实现 IFmattable 接口才能实现自定义的格式化参数。在一些场合还是觉得不太方便或者说代码冗余

.net 的 string.Format 静态方法还提供了重载方法,具体签名如下:

  public static string Format(IFormatProvider provider, string format, params Object[] args)

这个方法比起原来使用的方法最前面增加了IFormatProvider 类型参数。使用此方法的优点是不需要为后面的参数对象实现 IFormattable 接口就可以自定义的格式化参数。这样就能解决前面提到不方便和冗余的问题。

还是看实例分析问题:

    class Square
    {
        public string Name { get; set; }
        public double Side { get; set; }

        public override string ToString()
        {
            return string.Format($"{this.Name} (Side: {this.Side})");
        }
    }
Square
    class Rectangle
    {
        public string  Name { get; set; }
        public double Witdh { get; set; }
        public double Height { get; set; }
        public override string ToString()
        {
            return string.Format($"{this.Name} (Width: {this.Witdh}, Height: {this.Height})");
        }
    }
Rectangle
    class MyFormatProvider:IFormatProvider
    {
        public Object GetFormat(Type foramtType)
        {
            return new MyFormatter();
        }
    }
    class MyFormatter : ICustomFormatter
    {
        public string Format(string format, object arg, IFormatProvider formatProvider)
        {
            var t = "Hello";
            switch (format)
            {
                case "UPP":
                    t= t.ToUpper();
                    break;
                case "LOW":
                    t = t.ToLower();
                    break;
                default:
                    break;

            }

            return t + arg.ToString();
        }
    }
MyFormatter
    Square square = new Square() { Name = "MySquare", Side = 12.3 };
    Rectangle rectangle = new Rectangle() { Name = "MyRectangle", Witdh = 10.1, Height = 23.4 };

    var msg4 = string.Format(new MyFormatProvider(), "{0} {1}",square,rectangle);
    //var msg4 = string.Format(new MyFormatProvider(), "{square} {rectangle}");
    //这里用 $ 不能得到自己想要的结果,它实现不了重写的效果,存疑!!!
    Console.WriteLine(msg4);

    var msg5 = string.Format(new MyFormatProvider(), "{0:UPP} {1}",square,rectangle);
    Console.WriteLine(msg5);

    //这里从结果说明只要提供了new MyFormatProvider(),
    //那么执行过程是:根据MyFormatProvider 对象得到 MyFormatter对象,
    //利用MyFormatter 对象的Format方法进行格式化

 

这里还有一个问题 ,如果 MyFormatProvider 的 GetFormat 方法返回的不是一个实现了 ICustomFormatter 接口的对象又会是什么情况呢

答案是会报异常。返回是null的话,直接调用对象的ToString() 方法。

使用这种方式还能解决另外一种问题,假如我们已经为圆形类实现了 IFormattable 接口,并且已经实现了{0:UPP} 格式化参数,但是实现的方法中没有加{0:LOW}格式化参数,而且这个类我们又不能修改(可能是 .net 自带的类可能是第三方 dll 提供的类等等),那么这就不是IFormattable 接口能解决的了

这里通过上面方法就可以解决。

通过以上的例子我们就了解了如果我们需要定义一种通用的格式化方法的话,不需要让类实现IFormattable 接口,可以通过定义实现IFormatProvider,ICustomFormatter 接口的类去做,上面无论是正方形还是长方形类都需要在前面增加 Hello 进行格式化,可以是普通的,小写的,大写的等等(需要的效果可自行定义),不需要两个类单独去实现了,以后增加圆形、三角形等等,也能用我们已经定义好的 MyFormatProvider 和 MyFormatter 去进行格式化。

 

posted @ 2022-07-11 09:31  xunzf  阅读(86)  评论(0)    收藏  举报