ARTICLE AD BOX
I don't think there is a (easy and flexible) way of accessing a ref struct property (in this reflection-like manner) without knowing the return type at compile time, via a type parameter. Honestly, your current implementation using expression trees seems like the best solution for your problem. It's clean and flexible.
ref struct are allocated on the stack and are guaranteed not to escape to the managed heap. This significantly confines how our custom GetProperty method can be implemented. We cannot define a broad return type of System.ValueType or System.Object. To my knowledge, we also cannot return a pointer to the ref struct because the memory associated with that stack frame would have already been deallocated by the time we access the struct.
The standard Func<T> and Action<T> delegates also don't allow T to be a ref struct. Yes, we could define custom delegates like the one below, but that's essentially the same as explicitly declaring a return type, which you don't want. The example below shows how that looks.
CreateDelegate returns a delegate that returns a ref struct of a specific type T. This fixed T nature is a limitation of this approach: you would have to define a new delegate for each return type.
public static class Helpers { public delegate ReadOnlySpan<char> GetSpan<T>(T host); public static TDelegate CreateDelegate<TTarget, TDelegate>(string propertyName) where TDelegate : Delegate { PropertyInfo? property = typeof(TTarget).GetProperty(propertyName); // assume property exists MethodInfo? getter = property!.GetGetMethod(); // assume getter exists return (TDelegate)getter!.CreateDelegate(typeof(TDelegate)); } }Usage would look like this:
Match match = new Regex("\\d+").Match("testing 123"); Helpers.GetSpan<Match> del = Helpers.CreateDelegate<Match, Helpers.GetSpan<Match>>(nameof(Match.ValueSpan)); Console.WriteLine(del(match).ToString());Another approach removes the return type declaration from the method signature entirely. I don't think it's practical, but it's "hacky" and probably unstable. Essentially, we get the raw bytes of the property value and store them as a managed array (this removes any ref struct constraint issues). After that, we reconstruct the bytes into the desired type. My initial inspiration came from here.
public static class Helpers { public static byte[] GetProperty(object host, string name) { Type hostType = host.GetType(); PropertyInfo? property = hostType.GetProperty(name); // assume property exists MethodInfo? getter = property!.GetGetMethod(); // assume getter exists Type genericDef = property.PropertyType.GetGenericTypeDefinition(); Type elementType = property.PropertyType.GetGenericArguments()[0]; // we need to get respective method: param of type Span<T> or param of type ReadOnlySpan<T> MethodInfo asBytes = typeof(MemoryMarshal) .GetMethods() .Where(m => m.Name == nameof(MemoryMarshal.AsBytes) && m.IsGenericMethod) .Single(m => { Type param = m.GetParameters()[0].ParameterType; return param.IsGenericType && param.GetGenericTypeDefinition() == genericDef; }); MethodInfo toArray = typeof(Span<byte>).GetMethod(nameof(Span<byte>.ToArray))!; DynamicMethod dm = new DynamicMethod( "converter", typeof(byte[]), new []{ typeof(object) }, hostType, true ); ILGenerator il = dm.GetILGenerator(); LocalBuilder spanLocal = il.DeclareLocal(typeof(Span<byte>)); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Castclass, hostType); il.Emit(OpCodes.Callvirt, getter!); il.Emit(OpCodes.Call, asBytes.MakeGenericMethod(elementType)); il.Emit(OpCodes.Stloc, spanLocal); il.Emit(OpCodes.Ldloca, spanLocal); il.Emit(OpCodes.Call, toArray); il.Emit(OpCodes.Ret); Func<object, byte[]> call = (Func<object, byte[]>)dm.CreateDelegate(typeof(Func<object, byte[]>)); return call(host); } }Usage would look like this:
Match match = new Regex("\\d+").Match("testing 123"); byte[] bytes = Helpers.GetProperty(match, "ValueSpan"); ReadOnlySpan<char> recovered = MemoryMarshal.Cast<byte, char>(bytes); Console.WriteLine(recovered.ToString());I'm not insane enough to use this in practice, but it exists. Really, though, I don't see the need to declare the return type as a function parameter instead of a type parameter like you mentioned. What you have is fine. Unless you have an extremely niche goal (I don't know).
