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}\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); }));
}

 

運行結果如下:

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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM