Hello World
Spiga

如何创建TextWriter的子类

2009-09-11 00:42 by 老赵, 12813 visits

如果您需要继承TextWriter实现自己的类型,您会怎么做?继承TextWriter不难,不过接下来,您打算覆盖(override)掉哪些方法?今天我就遇到了这样的问题。还是先来看看TextWriter的成员吧:

[Serializable]
[ComVisible(true)]
public abstract class TextWriter : MarshalByRefObject, IDisposable
{
    protected char[] CoreNewLine;
    public static readonly TextWriter Null;

    protected TextWriter();
    protected TextWriter(IFormatProvider formatProvider);

    public abstract Encoding Encoding { get; }
    public virtual IFormatProvider FormatProvider { get; }
    public virtual string NewLine { get; set; }

    public virtual void Close();
    public void Dispose();
    protected virtual void Dispose(bool disposing);
    public virtual void Flush();
    public static TextWriter Synchronized(TextWriter writer);
    public virtual void Write(bool value);
    public virtual void Write(char value);
    public virtual void Write(char[] buffer);
    public virtual void Write(decimal value);
    public virtual void Write(double value);
    public virtual void Write(float value);
    public virtual void Write(int value);
    public virtual void Write(long value);
    public virtual void Write(object value);
    public virtual void Write(string value);
    [CLSCompliant(false)]
    public virtual void Write(uint value);
    [CLSCompliant(false)]
    public virtual void Write(ulong value);
    public virtual void Write(string format, object arg0);
    public virtual void Write(string format, params object[] arg);
    public virtual void Write(char[] buffer, int index, int count);
    public virtual void Write(string format, object arg0, object arg1);
    public virtual void Write(string format, object arg0, object arg1, object arg2);
    public virtual void WriteLine();
    public virtual void WriteLine(bool value);
    public virtual void WriteLine(char value);
    public virtual void WriteLine(char[] buffer);
    public virtual void WriteLine(decimal value);
    public virtual void WriteLine(double value);
    public virtual void WriteLine(float value);
    public virtual void WriteLine(int value);
    public virtual void WriteLine(long value);
    public virtual void WriteLine(object value);
    public virtual void WriteLine(string value);
    [CLSCompliant(false)]
    public virtual void WriteLine(uint value);
    [CLSCompliant(false)]
    public virtual void WriteLine(ulong value);
    public virtual void WriteLine(string format, object arg0);
    public virtual void WriteLine(string format, params object[] arg);
    public virtual void WriteLine(char[] buffer, int index, int count);
    public virtual void WriteLine(string format, object arg0, object arg1);
    public virtual void WriteLine(string format, object arg0, object arg1, object arg2);
}

看到这么多方法,每个都是virtual的,我真怕了。正如之前讨论的那样,遇到一堆一堆的virtual方法,最终确定需要从什么地方入手实在是一件极具挑战的事情。从Reflector的观察结果发现,其中所有的方法最终都会委托给这样一个空方法:

public override void Write(char value) { }

其他所有的方法,例如Write(string)方法,都会把需要输出的内容最终委托给Write(char)方法——例如拆成一个一个字符。这种做法的性能自然是比较差的(至少要多出很多Method Call,不是吗?),因此只覆盖Write(char)方法只能保证最终成果“可以运行”,却无法保证是最优秀的结果。但是又有谁可以告诉我,究竟该怎么做呢?

无奈之下,最终还是借助于Refactor,想要观察一下.NET框架内置的一些TextWriter是如何实现的。最终比较之下,发现StringWriter是一个不错的参考。因为它够简单,并且拥有了其他TextWriter子类所“共有”的扩展方式。简单地说,所有的Write(WriteLine)方法最终被“归类”为以下三种形式:

  • 写入单个字符
  • 写入字符串
  • 写入一个字符数组的一部分

表示成代码则是:

public class MyTextWriter : TextWriter
{
    public override void Write(char value) { ... }

    public override void Write(string value) { ... }

    public override void Write(char[] buffer, int index, int count) { ... }
}

例如StringWriter会将这些内容写入至内部的StringBuilder对象中。其他如StreamWriter等TextWriter的子类也几乎都是这样,看来这就是微软认为创建TextWriter的“最佳实践”了。

值得一提的是,在TextWriter的Close和Dispose的方法都会调用GC.SuppressFinalize(this):

public class TextWriter : MarshalByRefObject, IDisposable 
{
    public virtual void Close()
    {
        this.Dispose(true);
        GC.SuppressFinalize(this);
    }

    public void Dispose()
    {
        this.Dispose(true);
        GC.SuppressFinalize(this);
    }

    // other members
}

这意味着如果我们的Writer是在与非托管资源打交道的话,可以构造一个析构函数(Finalizer)由GC作最后一道防线。如果用户明确调用了Close或Dispose方法,则GC.SuppressFinalize(this)可以避免对象进入“析构队列”,这样对象便可以得到快速释放。但是,如果一个TextWriter的子类明确不会和非托管资源打交道的话,则GC.SuppressFinalize(this)也是一种无谓的浪费。因此,StringWriter同时还重写了TextWriter的Close方法:

public class StringWriter : TextWriter
{
    public override void Close()
    {
        this.Dispose(true);
    }

    // other members
}

当然,如果是一个“有可能”会涉及到非托管资源的TextWriter,如StreamWriter,或者是一个TextWriter的封装类,那么还是保留GC.SuppressFinalizer(this)比较妥当,毕竟进入Finalizer队列的性能开销比这GC.SuppressFinalizer的性能损耗要严重得多。

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

28 条回复

  1. 看看时间[未注册用户]
    *.*.*.*
    链接

    看看时间[未注册用户] 2009-09-11 01:42:00

    这都几点了 - -! 你强....
    我记得好像System.IO里面的StreamReader,StreamWrite都是装饰的Stream类 然后继承TextWriter...

  2. dark
    *.*.*.*
    链接

    dark 2009-09-11 01:47:00

    ( ⊙ o ⊙ )啊! 呼呼 上面的也是偶 俺终于坐了会滴1楼滴 - -! O(∩_∩)O哈哈~

  3. iceboundrock
    *.*.*.*
    链接

    iceboundrock 2009-09-11 07:47:00

    不写GC.SuppressFinalizer(this)不会泄漏资源啊。按我的理解,按照一般的Dispose模式,Finalize中是调用Dispose来确保不会泄漏非托管资源的。
    GC.SuppressFinalizer(this)写在Dispose/Close方法中,是因为已经显式的释放了非托管资源,所以用这个方法告诉GC可以直接释放this的内存而不需要把this放入Finalizer Queue,再等Finalizer thread执行this的Finalize方法了。同理,因为这时this中的非内存资源已经在Close/Dispose中被释放掉了,所以即使不写GC.SuppressFinalizer而导致this进入Finalizer Queue,延迟释放的只是内存而已。

  4. 老赵
    admin
    链接

    老赵 2009-09-11 08:44:00

    @iceboundrock
    是不会泄露资源啊,我文章里说了:“这意味着如果我们的Writer是在与非托管资源打交道的话,可以构造一个析构函数(Finalizer)由GC作最后一道防线。如果用户明确调用了Close或Dispose方法,则GC.SuppressFinalize(this)可以避免对象进入“析构队列”,这样对象便可以得到快速释放。”

  5. 老赵
    admin
    链接

    老赵 2009-09-11 08:46:00

    @iceboundrock
    不过最后两句话的确有问题,改了,瞅瞅是不是没问题了,呵呵。

  6. 麒麟.NET
    *.*.*.*
    链接

    麒麟.NET 2009-09-11 08:47:00

    @看看时间
    各种Stream是装饰的Stream类,各种Writer/Reader是继承的TextWriter/TextReader。

  7. iceboundrock
    *.*.*.*
    链接

    iceboundrock 2009-09-11 08:54:00

    @Jeffrey Zhao
    现在没问题了。
    BTW:Dispose/Finalize这块,msdn里有篇不错的指导文章,里面还总结了一个需要实现IDispose的类型的基类。

    http://msdn.microsoft.com/en-us/library/b1yfkh5e.aspx

  8. 老赵
    admin
    链接

    老赵 2009-09-11 09:02:00

    @iceboundrock
    其实就是Disposable Pattern,我面试一直问的,嘿嘿。

  9. iceboundrock
    *.*.*.*
    链接

    iceboundrock 2009-09-11 10:30:00

    @Jeffrey Zhao
    哈哈,看来以后你面试可以不用这题了,已经泄密了。

  10. 老赵
    admin
    链接

    老赵 2009-09-11 10:40:00

    @iceboundrock
    我敢打赌,即使这样,大部分人还是搞不清楚的。

  11. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-09-11 11:59:00

    Jeffrey Zhao:
    @iceboundrock
    我敢打赌,即使这样,大部分人还是搞不清楚的。




    面试的时候可以选择的题目很多的,不过倒是有可能会有很多人特意去背这些东西。。。


    So,我面试的时候,出的题目都是网上怎么都找不到答案的。。。。
    如果他简历上写熟悉C,就会问const是不是C的关键字,还是C++才有的?这个不难,答出来了就向前推进,signed呢?条件编译的条件是?#define的语法是?typedef是?
    如果写着熟悉SQL Server又熟悉Oracle,就可以问问,感觉这两个数据库有啥不同?从使用架构各方面都可以。

    可惜我最近面了四个人,其中三个人无法写出委托类型如何定义,没有用过匿名方法,yield就不敢继续问了,工作经验都是两三年了,一直用VS2005做开发。我觉得有点浪费,不如用2003。令我大跌眼镜的是,一个工作经验才两个月的家伙,把这些问题全答上来了,尽管匿名方法的语法还是有点儿瑕疵。

  12. 老赵
    admin
    链接

    老赵 2009-09-11 12:04:00

    @Ivony...
    在我们这个行业,年份大都是假的。混五年的人很多,远不如踏踏实实走一年的人。
    所以我基本上不看年份这个“数字”,而且看不管那些叫着自己什么工作五年却找不到好工作,然后还怪罪搞技术或搞.NET没前途的人,呵呵。

  13. 韦恩卑鄙
    *.*.*.*
    链接

    韦恩卑鄙 2009-09-11 12:07:00

    哈哈 楼上的问题两个月前我也不会

  14. 韦恩卑鄙
    *.*.*.*
    链接

    韦恩卑鄙 2009-09-11 12:08:00

    被项目逼着往前走 没有时间回头看看的人也很多

  15. 钧梓昊逑
    *.*.*.*
    链接

    钧梓昊逑 2009-09-11 12:09:00

    我去你那面试一把?

  16. 老赵
    admin
    链接

    老赵 2009-09-11 12:11:00

    @韦恩卑鄙
    我不知道到底会被逼成什么样子。
    我去年一年也被逼过,每天9点到公司,晚上11点离开,照样可以学很多。
    我觉得是工作方式的问题,有意识去学的人,总归可以学到东西。

  17. 韦恩卑鄙
    *.*.*.*
    链接

    韦恩卑鄙 2009-09-11 12:17:00

    我得承认 wower的精力都在服务器算法的黑箱预测上了
    这afk的一年才有时间好好总结下
    :D

  18. 鹤冲天
    *.*.*.*
    链接

    鹤冲天 2009-09-11 12:18:00

    请教老赵一个相关的问题,String类有如下五个Format静态方法:

    public sealed class String : ...
    {
        public static string Format(string format, params object[] args);
        public static string Format(string format, object arg0);
        public static string Format(string format, object arg0, object arg1);
        public static string Format(string format, object arg0, object arg1, object arg2);
        public static string Format(IFormatProvider provider, string format, params object[] args);
    }
    

    前四个都将格式化的任务委托给了最后一个。第一个方法是为了方便我们使用,但中间三个,感觉没必要存在,不知道为什么要这样设计?

  19. 老赵
    admin
    链接

    老赵 2009-09-11 12:35:00

    @鹤冲天
    性能。如果是3个的话,Format(string, object, object, object)性能比Format(string, params object[])要高。
    这是上次在.NET大会上Jeffrey Richter说的,也符合性能测试结果,至于是否真是这个目的似乎较难考证。

  20. 鹤冲天
    *.*.*.*
    链接

    鹤冲天 2009-09-11 12:47:00

        public static string Format(string format, params object[] args)
        {
            return Format(null, format, args);
        }
        public static string Format(string format, object arg0, object arg1, object arg2)
        {
            return Format(null, format, new object[] { arg0, arg1, arg2 });
        }
    

    可从反编译表面上看来,Format(string, object, object, object)要生成一个数组。
    难倒,莫非是因为“params”?!它也生成一个数组。

  21. 韦恩卑鄙
    *.*.*.*
    链接

    韦恩卑鄙 2009-09-11 12:49:00

    是的 就是params 建立数组效率比较低

  22. 韦恩卑鄙
    *.*.*.*
    链接

    韦恩卑鄙 2009-09-11 12:50:00

    所以jr说 大量的 params 设计的方法 都有
    Method ( object )
    Method ( object, object)
    Method ( object, object, object)
    的版本

  23. 老赵
    admin
    链接

    老赵 2009-09-11 12:54:00

    @韦恩卑鄙
    JR这个说法不错的。

  24. 鹤冲天
    *.*.*.*
    链接

    鹤冲天 2009-09-11 13:02:00

    呵呵,明白了。

  25. Colin Han
    *.*.*.*
    链接

    Colin Han 2009-09-12 21:13:00

    韦恩卑鄙:是的 就是params 建立数组效率比较低


    我设想,params完全可以在编译时进行转换啊。因此,应该没有性能问题啊。

  26. 老赵
    admin
    链接

    老赵 2009-09-12 21:19:00

    @Colin Han
    是在编译时指定,但是构造一个长度为3的数组,比传3个参数要慢咯。

  27. Colin Han
    *.*.*.*
    链接

    Colin Han 2009-09-12 21:51:00

    @Jeffrey Zhao

    鹤冲天:

        public static string Format(string format, params object[] args)
        {
            return Format(null, format, args);
        }
        public static string Format(string format, object arg0, object arg1, object arg2)
        {
            return Format(null, format, new object[] { arg0, arg1, arg2 });
        }
    

    可从反编译表面上看来,Format(string, object, object, object)要生成一个数组。
    难倒,莫非是因为“params”?!它也生成一个数组。



    呵呵,这里还是构造了一个数组。

  28. 老赵
    admin
    链接

    老赵 2009-09-12 21:55:00

    @Colin Han
    这就不知道了,实验的时候是空方法。

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我