Избегайте циклических ссылок для классов

#c# #recursion #circular-dependency #circular-reference

#c# #рекурсия #циклическая зависимость #циклическая ссылка

Вопрос:

У меня есть функция, которая создает и возвращает «Person». У «Лица» есть свойство «Супруга», которое, конечно же, является другим «лицом». Это вызывает бесконечную рекурсию, поскольку создаваемый «Пользователь» всегда является «новым» при каждом вызове функции. Есть ли способ использовать ту же функцию (как показано ниже), не вызывая бесконечного цикла?

 public PersonModel Load(int personID)
{
    PersonModel person = new PersonModel();
    using (SqlConnection conn = new SqlConnection())
    {
        conn.ConnectionString = Helpers.ConnectionDB;
        SqlCommand command = new SqlCommand();
        command.Connection = conn;
        command.CommandType = CommandType.StoredProcedure;
        command.CommandText = "LoadPerson";
        command.Parameters.Add(new SqlParameter("@PersonID", personID));
        conn.Open();
        SqlDataReader reader = command.ExecuteReader();
        if (reader.Read())
        {
            person.PersonID = int.Parse(reader["PersonID"].ToString());
            person.FirstName = reader["FirstName"].ToString();
            person.LastName = reader["LastName"].ToString();
            person.MiddleName = reader["MiddleName"].ToString();
            person.Age = reader["Age"] != DBNull.Value ? int.Parse(reader["Age"].ToString()) : (int?)null;
            person.SpouseID = reader["SpouseID"] != DBNull.Value ? int.Parse(reader["SpouseID"].ToString()) : (int?)null;
            if (person.SpouseID != null amp;amp; person.Spouse == null)
            {
                person.Spouse = this.Load(person.SpouseID.Value);
            }
        }
        conn.Close();
    }
    return person;
}
  

Комментарии:

1. Возможно, вам лучше написать рекурсивный CTE для загрузки социального графа вместо того, чтобы делать это в коде приложения, так он будет менее привязан к сети и потребует выполнения только одной команды.

2. В подобном случае, когда A ссылается на B, а B ссылается на A, я бы только удерживал только идентификатор супруга, а не объект Person. Если вы хотите получить сведения о супруге, то вы создаете супруга из идентификатора. В противном случае вы столкнетесь с проблемами при попытке сериализации такого рода отношений.

3. Dai — я не знаком с тем, что вы имеете в виду. Вы предлагаете мне загрузить супруга (если он есть) в самом sproc, а затем создать этот объект из возвращаемой мной вторичной таблицы? Я надеялся избежать этого, поскольку у меня будет аналогичная функция, которая возвращает ВСЕХ «лиц» (в отличие от только определенного).

4. Добавьте 2-й параметр в список параметров bool-супруга. Вызывайте функцию только 2-й раз, когда значение супруга равно false .

Ответ №1:

Используйте a Dictionary<Int32,PersonModel> для отслеживания загруженных объектов:

 public PersonModel Load(Dictionary<Int32,PersonModel> dict, int personId) {

    PersonModel ret;
    if( dict.TryGetValue( personId, out ret ) ) return ret;

    // load from database here, but do not call recursively just yet
    ret = new PersonModel() { ... };

    dict.Add( ret.PersonId, ret );

    ret.Spouse = this.Load( dict, person.SpouseId.Value );
}
  

Ответ №2:

Я могу придумать пару разных вариантов:

  1. Добавьте необязательный параметр Load() , чтобы указать, что вы не должны пытаться загрузить супруга. Это немного взлом, но очень просто.

     public PersonModel Load(int personID, bool loadSpouse = true)
    {
        ...
        if (loadSpouse amp;amp; person.SpouseID != null amp;amp; person.Spouse == null)
        {
            person.Spouse = this.Load(person.SpouseID.Value, false);
        }
        ...
    
        return person;
    }
      
  2. Выберите свойства супруга в сохраненной процедуре, используя левое внешнее соединение. Это будет работать лучше, и если вы когда-нибудь захотите выбрать всех сотрудников, это будет намного эффективнее, поскольку это будет только один доступ к базе данных. Вы бы добавили подобный код в оператор if для создания объекта-супруга:

     person.Spouse = new PersonModel()
    person.Spouse.FirstName = reader["SpouseFirstName"].ToString();
    ...
      

Ответ №3:

Не совсем в соответствии с вопросом OPs, но поскольку я попал сюда из-за названия, возможно, было бы полезно добавить общий ответ на этот вопрос.

Итак, в моем сценарии мне нужно было изменить свойство во всем графике, установив все его вспомогательные свойства на любую глубину.

Чтобы гарантировать, что я не получу экземпляр, выполняемый дважды (что позволяет StackOverflowException ), я передаю a HashSet<T> по каждому вызову, поэтому мы знаем, что нужно пропускать выполненные элементы:

 //The executed parameter needn't ever be provided externally.
static void SetState(IObject obj, State state, HashSet<IObject> executed = null)
{
  if(executed == null) executed = new HashSet<IObject>(/* provide comparer if needed*/);
  if(!executed.Add(obj)) return;

  //safe to continue walking graph
  obj.State = state;
  var props = obj.GetObjectPropertyValues().OfType<IObject>();
  foreach (var prop in props)
    SetState(obj, state, executed);
}
  

Обратите внимание, элементы должны реализовывать правильное равенство и GetHashCode функции, или аргумент средства сравнения должен быть передан конструктору HashSet ‘s.