Avatar ·

Как получить значение свойства объекта по его имени (строке) через рефлексию в C#?

Всем привет! Я новичок в C# и столкнулся с такой задачей: у меня есть класс `Person` с несколькими свойствами (`Name`, `Age`, `Email`). Мне нужно написать метод, который принимает объект этого класса и строку — название свойства, а возвращает значение этого свойства. То есть, например, я передаю `person` и `"Name"`, а получаю `"John"`. Я знаю, что это можно сделать через рефлексию (Reflection), но не понимаю, как правильно. Вот что я пробовал: ```csharp public class Person { public string Name { get; set; } public int Age { get; set; } public string Email { get; set; } } static object GetPropertyValue(object obj, string propertyName) { // Как тут получить значение? // Пробовал что-то вроде obj.GetType().GetProperty(propertyName), но не знаю, что дальше. return null; } ``` Проблема в том, что я не понимаю, как вызвать геттер свойства, если я знаю только его имя как строку. Нужно ли учитывать регистр? Как быть, если свойство называется, например, `IsActive`? И что делать, если свойства с таким именем нет — нужно вернуть null или выбросить исключение? Буду благодарен за подробный пример с объяснением. Заранее спасибо! <<>>

Основное решение: получаем значение свойства через рефлексию

Ваш инстинкт верен — нужно использовать GetProperty и затем GetValue. Вот полный рабочий метод с обработкой ошибок:

using System;
using System.Reflection;

public static object GetPropertyValue(object obj, string propertyName)
{
    if (obj == null)
        throw new ArgumentNullException(nameof(obj));
    if (string.IsNullOrWhiteSpace(propertyName))
        throw new ArgumentException("Имя свойства не может быть пустым", nameof(propertyName));

    // Получаем тип объекта
    Type type = obj.GetType();
    
    // Ищем свойство (по умолчанию — с учётом регистра, Public и Instance)
    PropertyInfo property = type.GetProperty(propertyName);
    
    if (property == null)
    {
        // Свойство не найдено — можно вернуть null или выбросить исключение
        // throw new ArgumentException($"Свойство '{propertyName}' не найдено в типе {type.Name}");
        return null; // Вариант: молча возвращаем null
    }
    
    // Проверяем, есть ли у свойства геттер
    if (!property.CanRead)
        throw new InvalidOperationException($"Свойство '{propertyName}' не имеет геттера");
    
    // Получаем значение
    return property.GetValue(obj);
}

// Пример использования:
Person person = new Person { Name = "John", Age = 30, Email = "john@example.com" };
object nameValue = GetPropertyValue(person, "Name");      // "John"
object ageValue = GetPropertyValue(person, "Age");        // 30 (boxed as object)
object missingValue = GetPropertyValue(person, "Address"); // null

Важные нюансы

  • Регистрозависимость: GetProperty по умолчанию чувствителен к регистру. Если нужно игнорировать регистр, используйте флаг BindingFlags.IgnoreCase:
    PropertyInfo property = type.GetProperty(propertyName, 
        BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
  • Типы-значения: Если свойство возвращает int, double и т.п., результат упаковывается (boxing) в object. Чтобы получить его обратно, нужно распаковать:
    int age = (int)GetPropertyValue(person, "Age");
  • Производительность: Рефлексия медленнее прямого доступа. Если метод вызывается часто (сотни тысяч раз), рассмотрите кэширование PropertyInfo в словаре или использование Delegate.CreateDelegate для создания типизированного делегата.

Расширенная версия с кэшированием

private static readonly Dictionary<Type, Dictionary<string, PropertyInfo>> _cache = 
    new Dictionary<Type, Dictionary<string, PropertyInfo>>();

public static object GetPropertyValueCached(object obj, string propertyName)
{
    Type type = obj.GetType();
    
    if (!_cache.TryGetValue(type, out var propertyDict))
    {
        propertyDict = new Dictionary<string, PropertyInfo>();
        _cache[type] = propertyDict;
    }
    
    if (!propertyDict.TryGetValue(propertyName, out var property))
    {
        property = type.GetProperty(propertyName);
        propertyDict[propertyName] = property;
    }
    
    return property?.GetValue(obj);
}

Современный подход (C# 9+)

Если вы используете .NET 5+, можно упростить код с помощью required и record, но для рефлексии это не меняет логики. Главное — помните про BindingFlags для непубличных свойств:

// Для получения private свойств:
PropertyInfo privateProp = type.GetProperty("PrivateProperty", 
    BindingFlags.NonPublic | BindingFlags.Instance);
<<>>

Альтернативный подход: использование динамических типов (ExpandoObject)

Если у вас нет строгой типизации и вы работаете с динамическими данными (например, JSON), можно избежать рефлексии, используя ExpandoObject и приведение к IDictionary<string, object>:

using System.Dynamic;

dynamic person = new ExpandoObject();
person.Name = "John";
person.Age = 30;

// Получаем значение по строке
var dict = (IDictionary<string, object>)person;
object name = dict["Name"]; // "John"
object age = dict["Age"];   // 30

Когда это полезно?

  • Когда структура объекта заранее неизвестна (например, при десериализации JSON без модели).
  • Когда нужно часто менять набор свойств.
  • Когда производительность не критична (рефлексия иногда быстрее, но ExpandoObject удобнее).

Недостатки

  • Потеря строгой типизации — ошибки в именах свойств обнаружатся только в рантайме.
  • Не подходит для существующих классов вроде Person — их нельзя просто так привести к ExpandoObject.

Для вашей задачи с классом Person рефлексия — правильный выбор. Но если вы когда-нибудь будете работать с динамическими данными, имейте в виду этот вариант.