我怎样才能避免这种无限循环?

时间:2009-05-25 00:58:19

标签: mvvm wrapper infinite-loop stack-overflow

感觉必须有一些半简单的解决方案,但我无法理解。

编辑:上一个例子更清楚地显示了无限循环,但这给出了更多的上下文。查看预编辑以快速了解问题。

以下2个类表示模型视图视图模型(MVVM)模式的视图模型。

/// <summary>
/// A UI-friendly wrapper for a Recipe
/// </summary>
public class RecipeViewModel : ViewModelBase
{
    /// <summary>
    /// Gets the wrapped Recipe
    /// </summary>
    public Recipe RecipeModel { get; private set; }

    private ObservableCollection<CategoryViewModel> categories = new ObservableCollection<CategoryViewModel>();

    /// <summary>
    /// Creates a new UI-friendly wrapper for a Recipe
    /// </summary>
    /// <param name="recipe">The Recipe to be wrapped</param>
    public RecipeViewModel(Recipe recipe)
    {
        this.RecipeModel = recipe;
        ((INotifyCollectionChanged)RecipeModel.Categories).CollectionChanged += BaseRecipeCategoriesCollectionChanged;

        foreach (var cat in RecipeModel.Categories)
        {
            var catVM = new CategoryViewModel(cat); //Causes infinite loop
            categories.AddIfNewAndNotNull(catVM);
        }
    }

    void BaseRecipeCategoriesCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        switch (e.Action)
        {
            case NotifyCollectionChangedAction.Add:
                categories.Add(new CategoryViewModel(e.NewItems[0] as Category));
                break;
            case NotifyCollectionChangedAction.Remove:
                categories.Remove(new CategoryViewModel(e.OldItems[0] as Category));
                break;
            default:
                throw new NotImplementedException();
        }
    }

    //Some Properties and other non-related things

    public ReadOnlyObservableCollection<CategoryViewModel> Categories 
    {
        get { return new ReadOnlyObservableCollection<CategoryViewModel>(categories); }
    }

    public void AddCategory(CategoryViewModel category)
    {
        RecipeModel.AddCategory(category.CategoryModel);
    }

    public void RemoveCategory(CategoryViewModel category)
    {
        RecipeModel.RemoveCategory(category.CategoryModel);
    }

    public override bool Equals(object obj)
    {
        var comparedRecipe = obj as RecipeViewModel;
        if (comparedRecipe == null)
        { return false; }
        return RecipeModel == comparedRecipe.RecipeModel;
    }

    public override int GetHashCode()
    {
        return RecipeModel.GetHashCode();
    }
}

/// <summary>
/// A UI-friendly wrapper for a Category
/// </summary>
public class CategoryViewModel : ViewModelBase
{
    /// <summary>
    /// Gets the wrapped Category
    /// </summary>
    public Category CategoryModel { get; private set; }

    private CategoryViewModel parent;
    private ObservableCollection<RecipeViewModel> recipes = new ObservableCollection<RecipeViewModel>();

    /// <summary>
    /// Creates a new UI-friendly wrapper for a Category
    /// </summary>
    /// <param name="category"></param>
    public CategoryViewModel(Category category)
    {
        this.CategoryModel = category;
        (category.DirectRecipes as INotifyCollectionChanged).CollectionChanged += baseCategoryDirectRecipesCollectionChanged;

        foreach (var item in category.DirectRecipes)
        {
            var recipeVM = new RecipeViewModel(item); //Causes infinite loop
            recipes.AddIfNewAndNotNull(recipeVM);
        }
    }

    /// <summary>
    /// Adds a recipe to this category
    /// </summary>
    /// <param name="recipe"></param>
    public void AddRecipe(RecipeViewModel recipe)
    {
        CategoryModel.AddRecipe(recipe.RecipeModel);
    }

    /// <summary>
    /// Removes a recipe from this category
    /// </summary>
    /// <param name="recipe"></param>
    public void RemoveRecipe(RecipeViewModel recipe)
    {
        CategoryModel.RemoveRecipe(recipe.RecipeModel);
    }

    /// <summary>
    /// A read-only collection of this category's recipes
    /// </summary>
    public ReadOnlyObservableCollection<RecipeViewModel> Recipes
    {
        get { return new ReadOnlyObservableCollection<RecipeViewModel>(recipes); }
    }


    private void baseCategoryDirectRecipesCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        switch (e.Action)
        {
            case NotifyCollectionChangedAction.Add:
                var recipeVM = new RecipeViewModel((Recipe)e.NewItems[0], this);
                recipes.AddIfNewAndNotNull(recipeVM);
                break;
            case NotifyCollectionChangedAction.Remove:
                recipes.Remove(new RecipeViewModel((Recipe)e.OldItems[0]));
                break;
            default:
                throw new NotImplementedException();
        }
    }

    /// <summary>
    /// Compares whether this object wraps the same Category as the parameter
    /// </summary>
    /// <param name="obj">The object to compare equality with</param>
    /// <returns>True if they wrap the same Category</returns>
    public override bool Equals(object obj)
    {
        var comparedCat = obj as CategoryViewModel;
        if(comparedCat == null)
        {return false;}
        return CategoryModel == comparedCat.CategoryModel;
    }

    /// <summary>
    /// Gets the hashcode of the wrapped Categry
    /// </summary>
    /// <returns>The hashcode</returns>
    public override int GetHashCode()
    {
        return CategoryModel.GetHashCode();
    }
}

除非有要求,否则我不打算显示模型(配方和类别),但它们基本上会处理业务逻辑(例如,将类别添加到类别中也会添加链接的另一端,即如果是category包含一个配方,然后配方也包含在该类别中)并且基本上决定了事情的进展。 ViewModels为WPF数据绑定提供了一个很好的接口。这就是包装类

的原因

由于无限循环在构造函数中并且它正在尝试创建新对象,我不能只设置一个布尔标志来防止这种情况,因为这两个对象都没有完成构造。

我正在考虑的是(作为单身人士或传递给构造函数或两者)a Dictionary<Recipe, RecipeViewModel>Dictionary<Category, CategoryViewModel>将延迟加载视图模型,但不创建新的一个如果已经存在,但我还没有到处试图看看它是否会起作用,因为它已经迟到了,我有点厌倦了在过去的6个小时左右处理这个问题。

不保证这里的代码会编译,因为我拿出了一堆与手头问题无关的东西。

8 个答案:

答案 0 :(得分:2)

回到原来的问题(和代码)。如果你想要的是拥有一个自动同步的多对多关系,那么请继续阅读。 寻找处理这些案例的复杂代码的最佳位置是任何ORM框架的源代码,这是该工具领域的常见问题。我将查看nHibernate(https://nhibernate.svn.sourceforge.net/svnroot/nhibernate/trunk/nhibernate/)的源代码,了解它如何实现处理1-N和M-N关系的集合。

您可以尝试的简单方法是创建自己的小集合类,只需要处理它。下面我删除了原始包装类并添加了一个BiList集合,该集合使用对象(集合的所有者)和属性另一面的名称进行初始化以保持同步(仅适用于MN,但是1- N很容易添加)。当然,你想要抛光代码:

using System.Collections.Generic;

public interface IBiList
{
    // Need this interface only to have a 'generic' way to set the other side
    void Add(object value, bool addOtherSide);
}

public class BiList<T> : List<T>, IBiList
{
    private object owner;
    private string otherSideFieldName;

    public BiList(object owner, string otherSideFieldName) {
        this.owner = owner;
        this.otherSideFieldName = otherSideFieldName;
    }

    public new void Add(T value) {
        // add and set the other side as well
        this.Add(value, true);
    }

    void IBiList.Add(object value, bool addOtherSide) {
        this.Add((T)value, addOtherSide);
    }

    public void Add(T value, bool addOtherSide) {
        // note: may check if already in the list/collection
        if (this.Contains(value))
            return;
        // actuall add the object to the list/collection
        base.Add(value);
        // set the other side
        if (addOtherSide && value != null) {
            System.Reflection.FieldInfo x = value.GetType().GetField(this.otherSideFieldName);
            IBiList otherSide = (IBiList) x.GetValue(value);
            // do not set the other side
            otherSide.Add(this.owner, false);
        }
    }
}

class Foo
{
    public BiList<Bar> MyBars;
    public Foo() {
        MyBars = new BiList<Bar>(this, "MyFoos");
    }
}

class Bar
{
    public BiList<Foo> MyFoos;
    public Bar() {
        MyFoos = new BiList<Foo>(this, "MyBars");
    }
}



public class App
{
    public static void Main()
    {
        System.Console.WriteLine("setting...");

        Foo testFoo = new Foo();
        Bar testBar = new Bar();
        Bar testBar2 = new Bar();
        testFoo.MyBars.Add(testBar);
        testFoo.MyBars.Add(testBar2);
        //testBar.MyFoos.Add(testFoo); // do not set this side, we expect it to be set automatically, but doing so will do no harm
        System.Console.WriteLine("getting foos from Bar...");
        foreach (object x in testBar.MyFoos)
        {
            System.Console.WriteLine("  foo:" + x);
        }
        System.Console.WriteLine("getting baars from Foo...");
        foreach (object x in testFoo.MyBars)
        {
            System.Console.WriteLine("  bar:" + x);
        }
    }
}

答案 1 :(得分:2)

首先 DI 不会解决您的问题,但有一件事总是与 DI 有关,它将解决您的问题是使用容器(或具有查找功能的上下文)

<强>解决方案:

您在这些地方编码失败:

var catVM = new CategoryViewModel(cat); //Causes infinite loop
...
var recipeVM = new RecipeViewModel(item); //Causes infinite loop

问题是由您为对象创建包装器(xxxViewModel)引起的,即使它已经存在。您需要检查此模型的包装器是否已存在并使用它来代替再次为同一对象创建包装器。因此,您需要一个Container来跟踪所有创建的对象。您的选择是:

选项-1:使用简单的工厂模式来创建对象,同时也跟踪它们:

class CategoryViewModelFactory
{
    // TODO: choose your own GOOD implementation - the way here is for code brevity only
    // Or add the logic to some other existing container
    private static IDictionary<Category, CategoryViewModel>  items = new Dictionary<Category, CategoryViewModel>();
    public static CategoryViewModel GetOrCreate(Category cat)
    {
        if (!items.ContainsKey(cat))
            items[cat] = new CategoryViewModel(cat);
        return items[cat];
    }
}

然后你在Recipe方面做了同样的事情并修复了有问题的代码:

  // OLD: Causes infinite loop
  //var catVM = new CategoryViewModel(cat);
  // NEW: Works 
  var catVM = CategoryViewModelFactory.GetOrCreate(cat);

注意:可能的内存泄漏?

您应该注意的一件事(这也是您不应该使用虚拟工厂实现的原因)是这些 creator 对象将会保持对模型对象及其View包装器的引用。因此,GC将无法从内存中清除它们。

选项-a1:很可能您的应用程序中已经有一个控制器(或上下文),视图可以访问。在这种情况下,我只是将GetOrCreate方法移动到此上下文而不是创建那些 a-la工厂。在这种情况下,当上下文消失(表单关闭)时,这些字典也将被取消引用,泄漏问题也就消失了。

答案 2 :(得分:1)

选项:

  1. 实施会员资格测试,例如在添加
  2. 之前检查bar-is-member-of-foo
  3. 将多对多关系移动到自己的类
  4. 后者是首选,我认为 - 它更具关系声音

    当然,以foo-bar为例我们真的不知道目标是什么,所以你的里程可能会有所不同

    编辑:给出原始问题中的代码,#1将无效,因为在将任何内容添加到任何列表之前无限递归发生。

    这种方法/问题存在一些问题,可能是因为它被抽象到接近愚蠢的程度 - 很好地说明了编码问题,对于解释原始意图/目标没有那么好:

    1. 包装类实际上不包装任何内容或添加任何有用的行为;这使得很难理解他们为什么需要
    2. 使用给定的结构,您无法在构造函数初始化列表,因为每个包装器列表会立即创建另一个包装器列表的新实例
    3. 即使你将初始化与构造分开,你仍然具有隐藏成员资格的循环依赖(即包装器相互引用但隐藏了包含检查的foo / bar元素;这并不重要,因为代码永远不会得到无论如何要在任何列表中添加任何内容!)
    4. 直接关系方法可行,但需要搜索机制并假设将根据需要而不是提前创建包装器,例如,带有搜索功能的数组或一对词典(例如Dictionary&gt;,Dictionary&gt;)可用于映射但可能不适合您的对象模型
    5. 结论

      我认为所描述的结构不起作用。不是DI,不是工厂,根本不是 - 因为包装器在隐藏子列表时互相引用。

      这个结构暗示了未说明的错误假设,但没有背景我们无法找出它们可能是什么。

      请使用真实世界的对象和所需的目标/意图在原始上下文中重述问题。

      或至少说明您认为示例代码应该生成的结构。 ; - )

      附录

      感谢您的澄清,这使情况更容易理解。

      我没有使用过WPF数据绑定 - 但是我已经浏览了this MSDN article - 所以以下内容可能有用也可能没有帮助和/或更正:

      • 我认为视图模型类中的类别和食谱集合是多余的
        • 您已经在基础类别对象中拥有M:M信息,因此为什么要在视图模型中复制它
        • 看起来你的集合更改处理程序也会导致无限递归
        • 收集更改的处理程序似乎不更新包装的配方/类别的基础M:M信息
      • 我认为视图模型的目的是公开底层模型数据,而不是单独包装其每个组件。
        • 这似乎是多余的,并且违反了封装
        • 它也是无限递归问题的根源
        • 天真地,我希望ObservableCollection属性只返回底层模型的集合......

      您拥有的结构是多对多关系的“反向索引”表示,这对于优化的查找和依赖关系管理来说非常常见。它减少为一对一对多的关系。查看MSDN文章中的GamesViewModel示例 - 请注意,Games属性只是

      ObservableCollection<Game>
      

      而不是

      ObservableCollection<GameWrapper>
      

答案 3 :(得分:1)

我建议你摆脱相互依赖,例如通过依赖倒置原则http://en.wikipedia.org/wiki/Dependency_inversion_principle - 至少有一方Foo和Bar(或它们的包装器)依赖于抽象接口另一方实现的,而不是两个具体的类直接相互依赖,这很容易产生循环依赖和相互递归的噩梦,就像你正在观察的那样。此外,还有其他方法可以实现可能值得考虑的多对多关系(并且通过引入合适的接口可能更容易受到依赖性的反转)。

答案 4 :(得分:1)

我要说Factory Pattern。这样,您可以依次构造每个,然后将它们相互添加,然后将它们全部隐藏在工厂的隐藏处。

答案 5 :(得分:1)

这让我想起了当对象包含其他对象时序列化防止无限循环的方式。它将每个对象的哈希码映射到其字节数组,因此当一个对象包含对另一个对象的引用时:a)不会将同一个对象序列化两次,并且b)不会将自身序列化为无限循环。

你的问题基本相同。解决方案可以像使用某种地图而不是列表集合一样简单。如果您获得的是多对多,那么您只需创建一个列表地图。

答案 6 :(得分:1)

伙计,我的答案并不像那些DI那么酷。但...

简单来说,我认为你必须在开始关联它们之前创建你的包装器。遍历整个Foos列表,创建FooWrappers。然后遍历Bars并创建BarWrappers。然后阅读源Foos,在相关的FooWrapper中为MyBarWrappers添加适当的BarWrapper引用,反之亦然。

如果您坚持同时为Foo实例创建一个包装器并立即创建与每个Bar实例的关系,那么您必须通过标记您正在处理的Foo(即Foo_1)来“中断”该循环,并让每个BarWrapper实例知道不要在它的MyFooWrappers集合中创建另一个FooWrapper_1实例。毕竟,事实上,你已经将FooWrapper_1创建得更高(或者更低,就像它)调用堆栈。

底线:作为代码健全的问题,包装器构造函数不应该创建更多的包装器。最重要的是 - 它应该只知道/发现每个Foo和Bar在其他地方存在一个唯一的包装器,并且只有在其他地方找不到它时,MAYBE才会创建包装器。

答案 7 :(得分:0)

所以,Foo和Bar是模特。 Foo是Bars的列表,Bar是Foos的列表。如果我正确读到你有两个对象,它们只不过是彼此的容器。 A是所有B的集合,B是所有As的集合?本质上不是那个循环吗?它的定义是无限递归。现实世界的案例是否包含更多行为?也许这就是为什么人们很难解释解决方案的原因。

我唯一的想法是,如果这是真的有意,那么使用静态类或使用静态变量来记录这些类只创建过一次。