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