• C# 在自定义的控制台输出重定向类中整合调用方信息


    C# 在自定义的控制台输出重定向类中整合调用方信息

    独立观察员 2021 年 1 月 6 日

    一、前言

    众所周知,在 .NET 的控制台应用程序(就是那种小黑框程序)中输出信息,使用的是控制台输出方法 Console.Write ("消息") 或 Console.WriteLine ("消息"),这两个方法称为标准输出。而在 Winform、WPF、网页程序中,使用这种方法输出的信息是没有地方显示的,在这些程序中,我们一般把信息输出到相应的显示控件中,或者写入日志中。

    比如我这有个 Winform 测试程序,相关按钮的后台逻辑就是向控制台输出 “哈哈哈”,一般情况下,点击这个按钮,左边的消息框将不会有任何消息输出:

    C# 在自定义的控制台输出重定向类中整合调用方信息插图

    二、输出重定向基础版

    但是这里却能显示出相关消息,是怎么回事呢?原来我在构造函数中添加了这么一句 —— Console.SetOut (new ConsoleWriter (ShowInfo));  —— 这就把原本输出到控制台的消息,重定向给了方法 ShowInfo 来进行输出,而 ShowInfo 方法内通过设置文本框的文本内容来达到了显示消息的效果:

    C# 在自定义的控制台输出重定向类中整合调用方信息插图1

    其中的关键就是自定义类 ConsoleWriter(后面有新版):

    using System;
    using System.IO;
    using System.Text;
    /*
     * 代码已托管 https://gitee.com/dlgcy/dotnetcodes/tree/dlgcy/DotNet.Utilities/ConsoleHelper
     */
    namespace DotNet.Utilities.ConsoleHelper
    {
        /// <summary>
        /// [dlgcy] Console 输出重定向
        /// 其他版本:DotNet.Utilities.WinformHelper.TextBoxWriter
        /// 用法示例:
        /// 在构造器里加上:Console.SetOut(new ConsoleWriter(s => { LogHelper.Write(s); }));
        /// </summary>
        /// <example>
        /// <code>
        /// public class Example
        /// {
        ///     public Example()
        ///     {
        ///         Console.SetOut(new ConsoleWriter(s => { LogHelper.Write(s); }));
        ///     }
        /// }
        /// </code>
        /// </example>
        public class ConsoleWriter : TextWriter
        {
            private readonly Action<string> _Write;
            private readonly Action<string> _WriteLine;
     
            /// <summary>
            /// Console 输出重定向
            /// </summary>
            /// <param name="write">日志方法委托(针对于 Write)</param>
            /// <param name="writeLine">日志方法委托(针对于 WriteLine)</param>
            public ConsoleWriter(Action<string> write, Action<string> writeLine)
            {
                _Write = write;
                _WriteLine = writeLine;
            }
     
            /// <summary>
            /// Console 输出重定向
            /// </summary>
            /// <param name="write">日志方法委托</param>
            public ConsoleWriter(Action<string> write)
            {
                _Write = write;
                _WriteLine = write;
            }
     
            // 使用 UTF-16 避免不必要的编码转换
            public override Encoding Encoding => Encoding.Unicode;
     
            // 最低限度需要重写的方法
            public override void Write(string value)
            {
                _Write(value);
            }
     
            // 为提高效率直接处理一行的输出
            public override void WriteLine(string value)
            {
                _WriteLine(value);
            }
        }
    }

    主要就是重写了 TextWriter 类的 Write 方法,然后在重写的 Write 方法中调用外部设置好的(通过构造函数)相关委托方法进行实际的信息输出。

    以上就是之前的版本,工作地还不错。不过,当我们想在记录信息时同时记录调用方的信息时,问题就来了。

    三、输出重定向进阶版(传递调用方信息)

    要记录方法的调用方信息,我们很容易想到可以使用 C#5.0 中新增的获取调用方信息的方式,话不多说,改造 ShowInfo 方法如下即可:

    /// <summary>
    /// 显示消息
    /// </summary>
    private void ShowInfo(string info, [CallerFilePath] string filePath = "", [CallerMemberName] string memberName = "", [CallerLineNumber] int lineNumber = 0)
    {
        TBInfo.Text += $"[{DateTime.Now:HH:mm:ss.ffff}][{filePath}][{memberName}][{lineNumber}] {info}
    
    ";
    }
     
    //private void ShowInfo(string info)
    //{
    //    TBInfo.Text += $"[{DateTime.Now:HH:mm:ss.ffff}] {info}
    
    ";
    //} 

    可以看到方法新增了以 CallerFilePath、CallerMemberName、CallerLineNumber 三个特性标注的三个可选参数,这样就能自动获得调用方法者的 文件名、成员名、行号了。

    自然,构造函数中的重定向方法也需要更改:

    public FormTest()
    {
        InitializeComponent();
     
        //Console.SetOut(new ConsoleWriter(ShowInfo));
        Console.SetOut(new ConsoleWriter(msg => { ShowInfo(msg); }));
    }

    运行结果如下:

    C# 在自定义的控制台输出重定向类中整合调用方信息插图2

    表面上看好像信息都有了,但是定睛一看,怎么调用成员显示的是 .ctor 而不是 BtnConsoleRedirect_Click ?行号显示的是 18 而不是 69?其实这里显示的信息是构造函数的(因为重定向语句在那里)。那么有没有办法显示实际的调用位置呢?我们继续改造。

    这次改造的是重定向类 ConsoleWriter:

    using System;
    using System.IO;
    using System.Text;
    /*
     * 代码已托管 https://gitee.com/dlgcy/dotnetcodes/tree/dlgcy/DotNet.Utilities/ConsoleHelper
     * 依赖:ClassHelper 类中获取调用信息的方法。
     */
    namespace DotNet.Utilities.ConsoleHelper
    {
        /// <summary>
        /// [dlgcy] Console 输出重定向
        /// 其他版本:DotNet.Utilities.WinformHelper.TextBoxWriter
        /// 用法示例:
        /// 在构造器里加上:Console.SetOut(new ConsoleWriter(s => { LogHelper.Write(s); }));
        /// </summary>
        /// <example>
        /// <code>
        /// public class Example
        /// {
        ///     public Example()
        ///     {
        ///         Console.SetOut(new ConsoleWriter(s => { LogHelper.Write(s); }));
        ///     }
        /// }
        /// </code>
        /// </example>
        public class ConsoleWriter : TextWriter
        {
            private readonly Action<string> _Write;
            private readonly Action<string> _WriteLine;
            private readonly Action<string, string, string, int> _WriteCallerInfo;
     
            /// <summary>
            /// Console 输出重定向
            /// </summary>
            /// <param name="write">日志方法委托(针对于 Write)</param>
            /// <param name="writeLine">日志方法委托(针对于 WriteLine)</param>
            public ConsoleWriter(Action<string> write, Action<string> writeLine)
            {
                _Write = write;
                _WriteLine = writeLine;
            }
     
            /// <summary>
            /// Console 输出重定向
            /// </summary>
            /// <param name="write">日志方法委托</param>
            public ConsoleWriter(Action<string> write)
            {
                _Write = write;
                _WriteLine = write;
            }
     
            /// <summary>
            /// Console 输出重定向(带调用方信息)
            /// </summary>
            /// <param name="write">日志方法委托(后三个参数为 CallerFilePath、CallerMemberName、CallerLineNumber)</param>
            public ConsoleWriter(Action<string, string, string, int> write)
            {
                _WriteCallerInfo = write;
            }
     
            /// <summary>
            /// 使用 UTF-16 避免不必要的编码转换
            /// </summary>
            public override Encoding Encoding => Encoding.Unicode;
     
            /// <summary>
            /// 最低限度需要重写的方法
            /// </summary>
            /// <param name="value">消息</param>
            public override void Write(string value)
            {
                if (_WriteCallerInfo != null)
                {
                    WriteWithCallerInfo(value);
                    return;
                }
     
                _Write(value);
            }
     
            /// <summary>
            /// 为提高效率直接处理一行的输出
            /// </summary>
            /// <param name="value">消息</param>
            public override void WriteLine(string value)
            {
                if (_WriteCallerInfo != null)
                {
                    WriteWithCallerInfo(value);
                    return;
                }
     
                _WriteLine(value);
            }
     
            /// <summary>
            /// 带调用方信息进行写消息
            /// </summary>
            /// <param name="value">消息</param>
            private void WriteWithCallerInfo(string value)
            {
                //3、System.Console.WriteLine -> 2、System.IO.TextWriter + SyncTextWriter.WriteLine -> 1、DotNet.Utilities.ConsoleHelper.ConsoleWriter.WriteLine -> 0、DotNet.Utilities.ConsoleHelper.ConsoleWriter.WriteWithCallerInfo
                var callInfo = ClassHelper.GetMethodInfo(4);
                _WriteCallerInfo(value, callInfo?.FileName, callInfo?.MethodName, callInfo?.LineNumber ?? 0);
            }
        }
    }

    即新增一个包含了调用方信息三个参数的委托 _WriteCallerInfo,以及配套的构造方法,然后在 Write 方法中优先使用 _WriteCallerInfo 委托方法。另外,引入了一个获取调用方信息的方法(改造自《C# 获取当前方法信息,上端调用方方法信息以及方法调用链》):

    using System;
    using System.Diagnostics;
    using System.IO;
    using System.Linq;
    using System.Reflection;
    using System.Runtime.Serialization.Formatters.Binary;
    /*
     * 代码已托管 https://gitee.com/dlgcy/dotnetcodes/tree/dlgcy/DotNet.Utilities/Object
     */
    namespace DotNet.Utilities
    {
        public class ClassHelper
        {
            #region 调用信息
     
            /* 参考:https://blog.csdn.net/m0_37886901/article/details/105266848 */
     
            /// <summary>
            /// 获取方法调用信息;
            /// </summary>
            /// <param name="index">0是本身,1是调用方,2是调用方的调用方...以此类推</param>
            /// <returns>MethodInfo 对象</returns>
            public static MethodInfo GetMethodInfo(int index)
            {
                try
                {
                    index++; //由于这里是封装了方法,相当于上端想要获取本身,其实对于这里而言,上端的本身就是这里的上端,所以需要+1,以此类推
                    var stack = new StackTrace(true);
     
                    //0是本身,1是调用方,2是调用方的调用方...以此类推
                    var currentFrame = stack.GetFrame(index);
                    var method = currentFrame.GetMethod();
                    var module = method.Module;
                    var declaringType = method.DeclaringType;
                    var stackFrames = stack.GetFrames();
     
                    string callChain = string.Join(" -> ", stackFrames.Select((r, i) =>
                    {
                        if (i == 0) return null;
                        var m = r.GetMethod();
                        return $"{m.DeclaringType.FullName}.{m.Name}";
                    }).Where(r => !string.IsNullOrWhiteSpace(r)).Reverse());
     
                    return new MethodInfo()
                    {
                        Method = method,
                        ModuleName = module.Name,
                        Namespace = declaringType.Namespace,
                        ClassName = declaringType.Name,
                        FullClassName = declaringType.FullName,
                        MethodName = method.Name,
                        CallChain = callChain,
                        LineNumber = currentFrame.GetFileLineNumber(),
                        FileName = currentFrame.GetFileName(),
                    };
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex);
                    return null;
                }
            }
     
            /// <summary>
            /// 方法调用信息
            /// </summary>
            public class MethodInfo
            {
                /// <summary>
                /// 方法完整信息;
                /// </summary>
                public MethodBase Method { get; set; }
     
                /// <summary>
                /// 模块名
                /// </summary>
                public string ModuleName { get; set; }
     
                /// <summary>
                /// 命名空间
                /// </summary>
                public string Namespace { get; set; }
     
                /// <summary>
                /// 类名
                /// </summary>
                public string ClassName { get; set; }
     
                /// <summary>
                /// 完整类名
                /// </summary>
                public string FullClassName { get; set; }
     
                /// <summary>
                /// 方法名
                /// </summary>
                public string MethodName { get; set; }
     
                /// <summary>
                /// 调用链
                /// </summary>
                public string CallChain { get; set; }
     
                /// <summary>
                /// 行号
                /// </summary>
                public int LineNumber { get; set; }
     
                /// <summary>
                /// 文件名
                /// </summary>
                public string FileName { get; set; }
            }
     
            #endregion
        }
    }

    最后,恢复测试程序构造函数处的重定向语句为之前的写法,自动识别为调用 ConsoleWriter 中我们新增的那个构造函数:

    C# 在自定义的控制台输出重定向类中整合调用方信息插图3

    运行,测试,可以看到方法名和行号都对了:

    C# 在自定义的控制台输出重定向类中整合调用方信息插图4

    四、后记及资源

    这种重定向的方式个人觉得挺方便的,比如在动态库中全都写成输出控制台的方式,然后在主程序构造函数中指定重定向;另外,还可用于转录到日志:

    C# 在自定义的控制台输出重定向类中整合调用方信息插图5

    上图所示的日志方法参见:《『简易日志』NuGet 日志包 SimpleLogger

    本文测试程序相关代码:https://gitee.com/dlgcy/dotnetcodes/tree/dlgcy/DotNet.Utilities.Test

    转录到日志的参考项目:https://gitee.com/dlgcy/WPFTemplate

  • 相关阅读:
    python笔记第二节
    python笔记第一节
    hadoop 家族生态圈 小结
    JDBC:impala 连接DEMO
    关于excel表格的导入导出的工具类。(java 反射 excel)
    简单写了一个类似全文检索的帮助类。
    关于框架SSM中文乱码问题总结
    关于SpringMVC, bean对象中含有Date型变,改如何接收数据的问题。
    Nlog
    java手动编译
  • 原文地址:https://www.cnblogs.com/weiliuhong/p/console-redirect-and-caller-info.html
Copyright © 2020-2023  润新知