ToString() 僅能提供單一格式輸出。本文逐步打造 ToString(string format) 擴展,支援屬性佔位符、格式化字串、巢狀屬性存取、集合屬性操作,讓任何物件都能像 DateTime、int 一樣靈活格式化輸出。
public static string ToString1(this object obj, string format)
{
var props = obj.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);
var pattern = string.Join("|", props.Select(p => p.Name));
return Regex.Replace(format, pattern, m =>
{
var val = props.First(p => p.Name == m.Value).GetValue(obj);
return val?.ToString() ?? "";
});
}
// 問題:無法區分「常量文字」與「屬性佔位符」
public static string ToString2(this object obj, string format)
{
var props = obj.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);
return Regex.Replace(format, @"\[(?<Name>[^\]]+)\]", m =>
{
var prop = props.FirstOrDefault(p => p.Name == m.Groups["Name"].Value);
if (prop == null) return m.Value; // 保留原樣
var val = prop.GetValue(obj);
return val?.ToString() ?? "";
});
}
// 用法: obj.ToString2("Name: [Name], Age: [Age]")
public static string ToString3(this object obj, string format)
{
var props = obj.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);
return Regex.Replace(format, @"\[(?<Name>[^\[\]:]+)(?::\s*(?<Format>[^\[\]:]+))?\]", m =>
{
var propName = m.Groups["Name"].Value;
var fmt = m.Groups["Format"].Value;
var prop = props.FirstOrDefault(p => p.Name == propName);
if (prop == null) return m.Value;
var val = prop.GetValue(obj);
return string.IsNullOrEmpty(fmt)
? val?.ToString() ?? ""
: string.Format("{0:" + fmt + "}", val);
});
}
// 用法: obj.ToString3("Birth: [Birthday:yyyy-MM-dd]")
public static string ToString4(this object obj, string format)
{
return Regex.Replace(format, @"\[(?<Name>[^\[\]:]+)(?::\s*(?<Format>[^\[\]:]+))?\]", m =>
{
var path = m.Groups["Name"].Value.Split('.');
var fmt = m.Groups["Format"].Value;
object current = obj;
try
{
foreach (var propName in path)
current = current.GetType().GetProperty(propName)?.GetValue(current);
}
catch { return m.Value; }
return string.IsNullOrEmpty(fmt)
? current?.ToString() ?? ""
: string.Format("{0:" + fmt + "}", current);
});
}
// 用法: person.ToString4("Grandson: [Son.Son.Name], Born: [Son.Son.Birthday:yyyy年MM月dd日]")
public static string ToString5(this object obj, string format)
{
return Regex.Replace(format, @"\[(?<Name>[^\[\]:]+)(?::\s*(?<Format>[^\[\]:]+))?\]", m =>
{
var path = m.Groups["Name"].Value.Split('.');
var fmt = m.Groups["Format"].Value;
object current = obj;
try
{
foreach (var propName in path)
current = current.GetType().GetProperty(propName)?.GetValue(current);
}
catch { return m.Value; }
if (string.IsNullOrEmpty(fmt)) return current?.ToString() ?? "";
// 集合特殊操作:.Count, .Name 等
if (fmt.StartsWith("."))
{
var op = fmt.Substring(1);
var enumerable = current as System.Collections.IEnumerable;
if (enumerable == null) return m.Value;
var items = enumerable.Cast<object>().ToArray();
return op switch
{
"Count" => items.Length.ToString(),
_ => string.Join(", ", items.Select(i => i.GetType().GetProperty(op)?.GetValue(i)?.ToString() ?? ""))
};
}
return string.Format("{0:" + fmt + "}", current);
});
}
// 用法: person.ToString5("Friends: [Friends.Count], Names: [Friends.Name]")
public static object GetPropertyValue(this object obj, string propertyName)
=> obj?.GetType().GetProperty(propertyName)?.GetValue(obj);
ConcurrentDictionary<Type, PropertyInfo[]> 快取反射結果。RegexOptions.Compiled 或靜態 Regex 實例。| 優點 | 缺點 |
|---|---|
| 零侵入,任意物件即用 | 反射效能較差,不適合高頻呼叫 |
| 格式字串外部化,支援多語系/樣板 | 編譯期無法檢查屬性名拼寫錯誤 |
| 支援巢狀、集合、格式化等豐富語法 | 複雜邏輯難以除錯 |
動態 ToString(format) 是原型開發、日誌記錄、報表樣板、序列化除錯的利器。生產環境高頻路徑建議改用原生 ToString() 覆寫或原始碼產生器 (Source Generator) 實現編譯期優化。