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.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 | |
{ | |
/* 参考: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; } | |
} | |
} | |
} |
最后,恢复测试程序构造函数处的重定向语句为之前的写法,自动识别为调用 ConsoleWriter 中我们新增的那个构造函数:
运行,测试,可以看到方法名和行号都对了:
四、后记及资源
这种重定向的方式个人觉得挺方便的,比如在动态库中全都写成输出控制台的方式,然后在主程序构造函数中指定重定向;另外,还可用于转录到日志:
本文测试程序相关代码:https://gitee.com/dlgcy/dotnetcodes/tree/dlgcy/DotNet.Utilities.Test
转录到日志的参考项目:https://gitee.com/dlgcy/WPFTemplate