0

Given this code:

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

This it possible to get the variables names from the Deconstruct method?

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?
    }
}

The idea to refactor the follow code to reduce the amount of repetition:

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

What I'm trying to solve with Test class:

var (id, level, message) = Regex
    .Match("123: qwe: qweasd", @"(?<id>\d+?): (?<level>\w+?): (?<message>\w+?):")
    .Groups
    .AsDeconstructable(); // 
Xiaoy312
  • 14,292
  • 1
  • 32
  • 44
  • 3
    Not that i am aware of (but me saying this does not preclude the existence of some overly complicated and nasty hack/trick). But the whole thing sounds like a XY problem to me. What are you really trying to achieve with a line like `var (c, d) = new Test();`? –  Nov 30 '18 at 20:27
  • @elgonzo I've updated my question with the real problem. – Xiaoy312 Nov 30 '18 at 20:40
  • Note that your tuple example suffers from the same name duplication as the `dynamic` example. You just moved the goal post a little: Instead having to duplicate the group names inside the Apply method, you now have to duplicate the group names in the tuple deconstruction. Different place, same name repetitions. –  Nov 30 '18 at 20:48
  • You can't create an anonymous class at runtime without a huge amount of work, but you could use another structure such as a `Dictionary` instead. Also I would suggest you have a misunderstanding about the repetition - there is a big difference between `Level` as an anonymous type field, and `"level"` as a string constant. – NetMage Nov 30 '18 at 20:48
  • What you could perhaps do is to use `ExpandoObject` (https://learn.microsoft.com/en-us/dotnet/api/system.dynamic.expandoobject?redirectedfrom=MSDN&view=netframework-4.7.2) as your `dynamic` object in your first code example. This way you can add fields dynamically to your `dynamic` object based on the named groups found in the Group collection of the regex... –  Nov 30 '18 at 20:51
  • @elgonzo The idea is to reduce the amount of repetition/copy-paste since that the error-prone part. And, the goal is to retrieve the names from the `Deconstruct` caller to fill the group names. Use `ExpandoObject` is no much different than using the pseudo-dictionary from the `GroupCollection` in the first place, or even worse since we can't rely on intellisense to autocomplete. – Xiaoy312 Nov 30 '18 at 21:24
  • How is it less errorprone to have to write `(id, level, message)`? How is that reducing amount of repetition? Using an ExpandoObject does not cause more repetition than having to explicitly write the tuple element names (variable names) like you want to... –  Nov 30 '18 at 21:27
  • @NetMage The `Match.Groups` is no much different than a dictionary, so there is no gain from that. I realise there is a gap between `.Level` as a field and `"level"` as a string. The goal was to close that gap. – Xiaoy312 Nov 30 '18 at 21:27
  • @elgonzo I agree with you, it is not any less error-prone. However, the idea was to cut down the amount of copy-paste/typing: `id = m.Group["id"].Value,` vs `(id)` – Xiaoy312 Nov 30 '18 at 21:31
  • 1
    I have no idea what you are getting at. Whether you use tuple-deconstruction or something else in your method, you will have to assign the value of `m.Group["id"].Value` to something, if you don't want to use/return the group collection itself. Why is it such a big dealbreaker to you whether the assignment would result in something that you could use like `result.id` vs. just only `id`? That feels more like you are scratching an itch for no reason; or i completely misunderstand where you want to go with this... –  Nov 30 '18 at 21:35
  • To reduce boilerplate code. – Xiaoy312 Nov 30 '18 at 21:39
  • What is `Apply` and where did it come from? – NetMage Nov 30 '18 at 21:47
  • Just a extension method to chain more fluently: public static TResult Apply(this T value, Func selector) => selector(value);` – Xiaoy312 Nov 30 '18 at 21:50
  • Possible duplicate of [I can't get parameter names from valuetuple via reflection in c# 7.0](https://stackoverflow.com/questions/43488888/i-cant-get-parameter-names-from-valuetuple-via-reflection-in-c-sharp-7-0) – Xiaoy312 Nov 30 '18 at 22:00
  • Note your example string doesn't actually work. – NetMage Nov 30 '18 at 22:01
  • "_To reduce boilerplate code._" How is trying to reduce `result.SomeName` further to `SomeName` reducing any code (boilerplate or not)? What you seem to attempt so labouriously is to save typing a dozen characters (more or less) here and there. This is not reducing boilerplate code, this is more like trying to shoot with artillery cannons at sparrows –  Dec 03 '18 at 15:15
  • I'm trying to reduce the `Prop = m.Group["PropKey"].Value` part. Because if it is possible to extract the names from name `ValueTuple` or `Deconstruct`, I can write an oneline extension method for parsing text value without have to generate a class while having explicitly named property (vesus `.ItemN`). – Xiaoy312 Dec 03 '18 at 22:22
  • And to illustrate, `var parsed match.ToValueTuple<(string level, string message)>();` is much concise than `var parsed = new { level = m.Group["level"].Value, message= m.Group["message"].Value };`. – Xiaoy312 Dec 03 '18 at 22:26
  • Now that C# 10 has added the `CallerArgumentExpression` attribute, I've added a [proposal](https://github.com/dotnet/csharplang/discussions/6071) to be able to use it with `Deconstruct` methods. – NetMage Apr 27 '22 at 19:35

2 Answers2

2

I really don't think this is a good idea, and Reflection can be terribly slow, but here we go.

First, we need some extensions to make dealing with properties and fields a little cleaner:

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

Then, we need to create a class to represent the desired result:

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

Now, we write an extension to map Group named values to properties or fields in an object:

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

Finally, we can use our new extension:

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

Note: it is possible to create anonymous types on the fly at runtime, but they are rarely useful. Since you don't know anywhere else in your code what the properties are, you must do everything through Reflection, and unless you are using the objects in a Reflection heavy environment like ASP.Net, you might as well use a Dictionary, or if you must, a DynamicObject (though, again, without knowing the field names that isn't too practical).

I added an additional extension to map Groups to a string[]. Since names for ValueTuple fields are only usable at compile time, creating an array and using indexes is just as good as creating a ValueTuple and using Item1, etc.

Finally, an attempt to work with an anonymous object. By passing in a template for the anonymous object, you can create a new anonymous object from the capture group values that have matching names.

Using a method extension for type inference:

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

Now you can pass in an anonymous type as a template and get a filled in anonymous object back. Note that only the fields in the anonymous type that match capture group names will be filled in, and no runtime error handling is done.

var parsed3 = Regex.Match("123: qwe: qweasd", @"(?<id>\d+?): (?<level>\w+?): (?<message>\w+?)")
                   .Apply(m => m.Success ? new { message = "", id = "", level = "" }.ToAnonymous(m) : null);
NetMage
  • 26,163
  • 3
  • 34
  • 55
  • The anonymous class is used to avoid creating a class in the first place, so this defeats the purpose. I wanted to move to `Deconstruct` or named tuple, since they will be less verbose than anonymous class. (like: `.ToValueTuple<(string id, string level)>()`). +1 for effort. – Xiaoy312 Nov 30 '18 at 22:48
  • @Xiaoy312 Unlike anonymous objects, fields in `ValueTuple`s are compile time only constructs. They don't show up in Reflection and can't be (easily) accessed at runtime. If anonymous tuples are okay, that can probably be done, perhaps a bit painfully. – NetMage Dec 03 '18 at 21:30
  • @Xiaoy312 Since `ValueTuple`s would not have names anyway at runtime, and since match values are all `string`s, I added a `string[]` version of the extension. – NetMage Dec 03 '18 at 21:41
  • Yeah, the names are erased at runtime for both named` ValueTuple` and `Deconstruct` method. I gave up on that route. Having a strings array really doesn`t bring much improvement to the code, especially since the mapping is lost. – Xiaoy312 Dec 03 '18 at 22:15
  • @Xiaoy312 So, what is wrong with `Dictionary`? BTW, even if you could get runtime access to the names (e.g. Reflection), what/how would you make use of that in your code? – NetMage Dec 03 '18 at 23:29
  • There is nothing wrong with dictionary, just less preferred in certain scenario for having access to a compile-time available `.SomePropertyOrField` that can be autocompleted. The `T` in `.ToValueTuple<(string id, string level)>()` is a named value tuple, and being named means that it can be autocompleted (`default(T).id`). If we can retrieve the names, we can dynamically instantiate an instance of `T` with the appropriate values. – Xiaoy312 Dec 03 '18 at 23:38
  • This essentially produces roughly the equivalent of `MakeObjectFromGroups()` without using a concrete class. – Xiaoy312 Dec 03 '18 at 23:41
  • But that isn't possible when the capture names are runtime constructs. Note that `ValueTuple` assigment is positional, and ignores the names. – NetMage Dec 04 '18 at 16:52
  • Hence, why I voted to close this question. https://stackoverflow.com/a/43510293/561113 did mentioned about name erasure. Thus making my attempt impossible. – Xiaoy312 Dec 04 '18 at 19:37
  • @Xiaoy312 You opened the question, I think you can delete it. Anyway, I am adding a way to bridge to an anonymous type by providing an annoying template that may get close to what you want? – NetMage Dec 04 '18 at 21:25
0

In C# 10, you can use the new attribute CallerArgumentExpression combined with out variable declaration in the call to get close to what you want:

First, a string extension method:

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

Now a sample ToVariables extension method for GroupCollection for arity 3 (obviously it is easy to create other 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;
    }
}

And now you can declare variables and assign them to group values from a Regex match with the only repetition in the pattern:

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

Note: An obvious extension would be to create ToVariables for Dictionary<string,T>. Another extension might be to fall back to positional groups when there are no named groups.

NetMage
  • 26,163
  • 3
  • 34
  • 55