为类型输出格式化字符串

继上一篇“C#:重写Equals时也要重写GetHashCode”后,本文继续讲《编写高质量代码改善C#程序的157个建议》一书第13个建议“为类型输出格式化字符串”。喜欢本书请到各大商城购买原书,支持正版。

正文开始:


有两种方法可以为类型提供格式化的字符串输出。一种是意识到类型会产生格式化字符串输出,于是让类型继承接口IFormattable。这对类型来说,是一种主动实现的方式,要求开发者可以预见类型在格式化方面的要求。更多的时候,类型的使用者需为类型自定义格式化器,这就是第二种方法,也是最灵活多变的方法,可以根据需求的变化为类型提供多个格式化器。下面就来详细介绍这两种方法。

最简单的字符串输出是为类型重写ToString方法,如果没有为类型重写该方法,默认会调用Object的ToString方法,它会返回当前类型的类型名称。但即使是重写了ToString方法,提供的字符串输出也是非常单一的,而通过实现IFormattable接口的ToString方法,可以让类型根据用户的输入而格式化输出。如下面这个类型Person,它本身提供了属性FirstName和LastName。现在,根据中文和英文语言地区的习惯,提供的ToString方法要支持输出“Hu Jessica”或“Jessica Hu”,实现代码应该如下所示:

class Person : IFormattable
{
  public string IDCode { get; set; }

  public string FirstName { get; set; }

  public string LastName { get; set; }

  /// <summary>
  /// 实现接口IFormattable 的方法ToString
  /// </summary>
  /// <param name="format"></param>
  /// <param name="formatProvider"></param>
  /// <returns></returns>
  public string ToString(string format, IFormatProvider formatProvider)
  {
    switch (format)
    {
      case "Ch":
        return this.ToString();
      case "Eg":
        return $"{FirstName} {LastName}";
      default:
        return this.ToString();
    }
  }

  /// <summary>
  /// 重写0bject.ToString()
  /// </summary>
  /// <returns></returns>
  public override string ToString()
  {
    return $"{LastName} {FirstName}";
  }
}

调用者代码如下所示:

Person person = new Person()
{
  FirstName = "Jessica",
  LastName = "Hu",
  IDCode = "NB123"
};
Console.WriteLine(person);
Console.WriteLine(person.ToString("Ch", null));
Console.WriteLine(person.ToString("Eg", null));

输出为:

Hu Jessica
Hu Jessica
Jessica Hu

上面这种方法是在意识到类型会存在格式化字符串输出方面的需求时,提前为类型继承了接口IFormattable。如果类型本身没有提供格式化输出的功能,这个时候,格式化器就派上了用场。格式化器的好处就是可以根据需求的变化,随时增加或者修改它。假设Person类如以下所示的实现。

class Person
{
  public string IDCode { get; set; }

  public string FirstName { get; set; }

  public string LastName { get; set; }
}

针对Person的格式化器的实现为:

class PersonFomatter : IFormatProvider, ICustomFormatter
{
  #region IFormatProvider 成员

  public object GetFormat(Type formatType)
  {
    if (formatType == typeof(ICustomFormatter))
      return this;
    else
      return null;
  }
  #endregion

  #region ICustomFormatter 成员

  public string Format(
      string format,
      object arg,
      IFormatProvider formatProvider
    )
  {
    Person person = arg as Person;
    if (person == null)
    {
      return string.Empty;
    }

    switch (format)
    {
      case "Ch":
        return $"{person.LastName} {person.FirstName}";
      case "Eg ":
        return $"{person.FirstName} {person.LastName}";
      case "ChM":
        return $"{person.LastName} {person.FirstName}:{person.IDCode}";
      default:
        return $"{person.FirstName} {person.LastName}";
    }
  }
  #endregion
}

一个典型的格式化器应该继承接口IFormatProvider 和 ICustomFomatter,所以应该像下面这样调用格式化器:

Person person = new Person()
{
  FirstName = "Jessica",
  LastName = "Hu",
  IDCode = "NB123"
};
Console.WriteLine(person.ToString());
PersonFomatter pFormatter = new PersonFomatter();
Console.WriteLine(pFormatter.Format("Ch", person, null));
Console.WriteLine(pFormatter.Format("Eg", person, null));
Console.WriteLine(pFormatter.Format("ChM", person, null));

输出为:

ConsoleAppForDotnet6.Person
Hu Jessica
Jessica Hu
Hu Jessica:NB123

本示例也演示了如果没有重写Object.ToString方法,类型会输出类型名称。

实际上,在第一个版本的Person类型中,如果对IFormattable的ToString方法稍作修改,就能让格式化输出在语法上支持更多的调用方式。注意看Person类型的最终版本中ToString方法的switch结构的default部分,如下所示。

class Person : IFormattable
{
  public string IDCode { get; set; }

  public string FirstName { get; set; }

  public string LastName { get; set; }

  /// <summary>
  /// 实现接口IFormattable 的方法ToString
  /// </summary>
  /// <param name="format"></param>
  /// <param name="formatProvider"></param>
  /// <returns></returns>
  public string ToString(string format, IFormatProvider formatProvider)
  {
    switch (format)
    {
      case "Ch":
        return this.ToString();
      case "Eg":
        return $"{FirstName} {LastName}";
      default:
        {
          // return this.ToString();
          ICustomFormatter customFormatter = formatProvider as ICustomFormatter;
          if (customFormatter == null)
          {
            return this.ToString();
          }
          return customFormatter.Format(format, this, null);
        }
    }
  }

  /// <summary>
  /// 重写0bject.ToString()
  /// </summary>
  /// <returns></returns>
  public override string ToString()
  {
    return $"{LastName} {FirstName}";
  }
}

最终,调用者的代码能够支持如下所示的语法:

Person person = new Person()
{
  FirstName = "Jessica",
  LastName = "Hu",
  IDCode = "NB123"
};
Console.WriteLine(person.ToString());
PersonFomatter pFormatter = new PersonFomatter();

// 第一类格式化输出语法
Console.WriteLine(pFormatter.Format("Ch", person, null));
Console.WriteLine(pFormatter.Format("Eg", person, null));
Console.WriteLine(pFormatter.Format("ChM", person, null));

// 第二类格式化输出语法,更简洁
Console.WriteLine(person.ToString("Ch", pFormatter));
Console.WriteLine(person.ToString("Eg", pFormatter));
Console.WriteLine(person.ToString("ChM", pFormatter));

下一篇我们接着读"建议14:正确实现浅拷贝和深拷贝",欢迎关注微信公众号【乐趣课堂】。

原文出处:微信公众号【乐趣课堂】

原文链接:https://mp.weixin.qq.com/s/1gipcrPWhWIT_LEGssHQZg

本文观点不代表Dotnet9立场,转载请联系原作者。

发表评论

登录后才能评论