C# 在自定义的控制台输出重定向类中整合调用方信息
目录
C# 在自定义的控制台输出重定向类中整合调用方信息
一、前言
二、输出重定向基础版
三、输出重定向进阶版(传递调用方信息)
四、后记及资源
独立观察员 2021 年 1 月 6 日
一、前言
众所周知,在 .NET 的控制台应用程序(就是那种小黑框程序)中输出信息,使用的是控制台输出方法 Console.Write ("消息") 或 Console.WriteLine ("消息"),这两个方法称为标准输出。而在 Winform、WPF、网页程序中,使用这种方法输出的信息是没有地方显示的,在这些程序中,我们一般把信息输出到相应的显示控件中,或者写入日志中。
比如我这有个 Winform 测试程序,相关按钮的后台逻辑就是向控制台输出 “哈哈哈”,一般情况下,点击这个按钮,左边的消息框将不会有任何消息输出:
二、输出重定向基础版
但是这里却能显示出相关消息,是怎么回事呢?原来我在构造函数中添加了这么一句 —— Console.SetOut (new ConsoleWriter (ShowInfo)); —— 这就把原本输出到控制台的消息,重定向给了方法 ShowInfo 来进行输出,而 ShowInfo 方法内通过设置文本框的文本内容来达到了显示消息的效果:
其中的关键就是自定义类 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}\r\n\r\n";
}//private void ShowInfo(string info)
//{
// TBInfo.Text += $"[{DateTime.Now:HH:mm:ss.ffff}] {info}\r\n\r\n";
//}
可以看到方法新增了以 CallerFilePath、CallerMemberName、CallerLineNumber 三个特性标注的三个可选参数,这样就能自动获得调用方法者的 文件名、成员名、行号了。
自然,构造函数中的重定向方法也需要更改:
public FormTest()
{InitializeComponent();//Console.SetOut(new ConsoleWriter(ShowInfo));Console.SetOut(new ConsoleWriter(msg => { ShowInfo(msg); }));
}
运行结果如下:
表面上看好像信息都有了,但是定睛一看,怎么调用成员显示的是 .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.WriteWithCallerInfovar 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 中我们新增的那个构造函数:
运行,测试,可以看到方法名和行号都对了:
四、后记及资源
这种重定向的方式个人觉得挺方便的,比如在动态库中全都写成输出控制台的方式,然后在主程序构造函数中指定重定向;另外,还可用于转录到日志:
上图所示的日志方法参见:《『简易日志』NuGet 日志包 SimpleLogger》
本文测试程序相关代码:https://gitee.com/dlgcy/dotnetcodes/tree/dlgcy/DotNet.Utilities.Test
转录到日志的参考项目:https://gitee.com/dlgcy/WPFTemplate