c#在对象解构期间获取变量的名称

c# Get name of variable(s) during object deconstruction

鉴于此代码:

var (c, d) = new Test();

这可以从 Deconstruct 方法中获取变量名称吗?

public class Test
{
    public void Deconstruct(out string value1, out string value2)
    {
        // is there a way to know the parameters are 
        // being mapped to "c" and "d" respectively
        // inside this method?
    }
}

重构以下代码以减少重复量的想法:

var parsed = Regex
    .Match("123: qwe: qweasd", @"(?<id>\d+?): (?<level>\d+?): (?<message>\d+?):")
    .Apply(m => !m.Success ? null : new 
    {
        // notice the names are repeated on both side
        ID = m.Group["id"].Value,
        Level = m.Group["level"].Value,
        Message = m.Group["message"].Value,
    });

我想用 Test 解决的问题 class:

var (id, level, message) = Regex
    .Match("123: qwe: qweasd", @"(?<id>\d+?): (?<level>\w+?): (?<message>\w+?):")
    .Groups
    .AsDeconstructable(); // 

我真的不认为这是个好主意,反射可能非常慢,但我们开始吧。

首先,我们需要一些扩展来使处理属性和字段更简洁:

public static class HelperExtensions {
    // ***
    // *** Type Extensions
    // ***
    public static List<MemberInfo> GetPropertiesOrFields(this Type t, BindingFlags bf = BindingFlags.Public | BindingFlags.Instance) =>
        t.GetMembers(bf).Where(mi => mi.MemberType == MemberTypes.Field | mi.MemberType == MemberTypes.Property).ToList();

    // ***
    // *** MemberInfo Extensions
    // ***
    public static void SetValue<T>(this MemberInfo member, object destObject, T value) {
        switch (member) {
            case FieldInfo mfi:
                mfi.SetValue(destObject, value);
                break;
            case PropertyInfo mpi:
                mpi.SetValue(destObject, value);
                break;
            default:
                throw new ArgumentException("MemberInfo must be of type FieldInfo or PropertyInfo", nameof(member));
        }
    }

    public static TOut Apply<TIn, TOut>(this TIn m, Func<TIn, TOut> applyFn) => applyFn(m);
}

然后,我们需要创建一个class来表示想要的结果:

public class ParsedMessage {
    public string ID;
    public string Level;
    public string Message;
}

现在,我们编写扩展以将 Group 命名值映射到对象中的属性或字段:

public static class MatchExt {
    public static T MakeObjectFromGroups<T>(this Match m) where T : new() {
        var members = typeof(T).GetPropertiesOrFields().ToDictionary(pf => pf.Name.ToLower());
        var ans = new T();
        foreach (Group g in m.Groups) {
            if (members.TryGetValue(g.Name.ToLower(), out var mi))
                mi.SetValue(ans, g.Value);
        }

        return ans;
    }

    public static string[] MakeArrayFromGroupValues(this Match m) {
        var ans = new string[m.Groups.Count-1];
        for (int j1 = 1; j1 < m.Groups.Count; ++j1)
            ans[j1-1] = m.Groups[j1].Value;

        return ans;
    }
}

最后,我们可以使用我们的新扩展了:

var parsed = Regex
    .Match("123: qwe: qweasd", @"(?<id>\d+?): (?<level>\w+?): (?<message>\w+?)")
    .Apply(m => m.Success ? m.MakeObjectFromGroups<ParsedMessage>() : null);

注意:可以在运行时即时创建匿名类型,但它们很少有用。由于您不知道代码中其他任何地方的属性是什么,因此您必须通过反射来完成所有操作,除非您在像 ASP.Net 这样的反射密集环境中使用对象,否则您不妨使用 Dictionary,或者如果必须的话,一个 DynamicObject(不过,同样,不知道不太实用的字段名称)。

我添加了一个额外的扩展来将 Groups 映射到 string[]。由于 ValueTuple 字段的名称仅在编译时可用,因此创建数组并使用索引与创建 ValueTuple 并使用 Item1

一样好

最后,尝试使用匿名对象。通过传入匿名对象的模板,您可以从具有匹配名称的捕获组值创建新的匿名对象。

使用方法扩展进行类型推断:

public static class ToAnonymousExt {
    public static T ToAnonymous<T>(this T patternV, Match m) {
        var it = typeof(T).GetPropertiesOrFields();
        var cd = m.Groups.Cast<Group>().ToDictionary(g => g.Name, g => g.Value);
        return (T)Activator.CreateInstance(typeof(T), Enumerable.Range(0, it.Count).Select(n => cd[it[n].Name]).ToArray());
    }
}

现在您可以将匿名类型作为模板传入,并返回填充的匿名对象。注意只有匿名类型中匹配捕获组名的字段才会被填充,不做任何运行时错误处理

var parsed3 = Regex.Match("123: qwe: qweasd", @"(?<id>\d+?): (?<level>\w+?): (?<message>\w+?)")
                   .Apply(m => m.Success ? new { message = "", id = "", level = "" }.ToAnonymous(m) : null);

在 C# 10 中,您可以在调用中使用新属性 CallerArgumentExpression 结合 out 变量声明来接近您想要的内容:

首先,一个string扩展方法:

public static class StringExt {
    public static string Past(this string s, string separator) {
        var starterPos = s.IndexOf(separator);
        return starterPos == -1 ? s : s.Substring(starterPos + separator.Length);
    }
}

现在一个示例 ToVariables GroupCollection 的 arity 3 的扩展方法(显然很容易创建其他 arities):

public static class GroupCollectionExt {
    public static void ToVariables(this GroupCollection gc, out string v1, out string v2, out string v3,
                                   [CallerArgumentExpression("v1")] string name1 = "", [CallerArgumentExpression("v2")] string name2 = "", [CallerArgumentExpression("v3")] string name3 = "") {
        v1 = gc.TryGetValue(name1.Past(" "), out var g1) ? g1.Value : default;
        v2 = gc.TryGetValue(name2.Past(" "), out var g2) ? g2.Value : default;
        v3 = gc.TryGetValue(name3.Past(" "), out var g3) ? g3.Value : default;
    }
}

现在您可以声明变量并将它们分配给来自 Regex 匹配的组值,模式中只有重复:

Regex
    .Match("123: qwe: qweasd:", @"(?<id>\d+?): (?<level>\w+?): (?<message>\w+?):")
    .Groups
    .ToVariables(out var id, out var level, out var message);

注意:一个明显的扩展是为 Dictionary<string,T> 创建 ToVariables。另一个扩展可能是在没有命名组时回退到位置组。