代码之家  ›  专栏  ›  技术社区  ›  Davy8

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

  •  2
  • Davy8  · 技术社区  · 17 年前

    我觉得这一定有个半简单的解决办法,但我就是想不出来。

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

    以下两个类表示模型视图模型的视图模型( 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();
        }
    }
    

    除非有要求,否则我不会费心展示模型(配方和类别),但它们基本上会处理业务逻辑(例如,将配方添加到类别也会添加链接的另一端,即,如果类别包含配方,那么配方也包含在该类别中),并基本上指示事情的发展方向。视图模型为WPF数据绑定提供了一个很好的接口。这就是包装类的原因

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

    我所想的是(作为一个单例或传递给构造函数或两者)有一个 Dictionary<Recipe, RecipeViewModel> Dictionary<Category, CategoryViewModel> 这将懒惰地加载视图模型,但如果已经存在,则不会创建一个新的模型,但我还没来得及尝试查看它是否会工作,因为它已经晚了,而且在过去的6个小时左右,我已经厌倦了处理这个问题。

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

    8 回复  |  直到 16 年前
        1
  •  1
  •   Jeff Meatball Yang    17 年前

    伙计,我的回答不像那些DI的答案那么酷。但是…

    最简单地说,我认为在开始关联包装纸之前,必须先创建包装纸。遍历整个foo列表,创建foowrapper。然后遍历条并创建条包装器。然后读取源foos,将适当的barwrapper引用添加到相关foowrapper中的mybarwrapper,反之亦然。

    如果您坚持为一个foo实例创建一个包装器并立即创建与它的每个bar实例的关系,那么您必须通过标记正在处理的foo(即foo_1)来“打破”这个循环,并让每个bar wrapper实例知道不要在它的myfoowrappers集合中创建另一个foo wrapper_1实例。毕竟,事实上,您已经在更高的(或者更低的)调用堆栈上创建了foowrapper_1。

    底线:考虑到代码的健全性,包装器构造函数不应该创建更多的包装器。最重要的是,它应该只知道/发现每个foo和bar在其他地方存在一个唯一的包装器,并且可能只有在其他地方找不到它时才创建包装器。

        2
  •  2
  •   van    16 年前

    回到你原来的问题(和代码)。如果您希望有一个自动同步的多对多关系,请继续阅读。 寻找处理这些情况的复杂代码的最佳位置是任何ORM框架的源代码,对于这个工具领域来说,这是非常常见的问题。我会看看NHibernate的源代码( https://nhibernate.svn.sourceforge.net/svnroot/nhibernate/trunk/nhibernate/ )了解它如何实现处理1-n和m-n关系的集合。

    您可以尝试创建自己的小集合类来处理这个问题。在下面,我删除了您的原始包装类并添加了一个bilist集合,该集合使用对象(集合的所有者)和要保持同步的属性另一端的名称初始化(仅适用于m-n,但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);
            }
        }
    }
    
        3
  •  2
  •   van    16 年前

    首先 不会解决你的问题,但总有一件事与 解决你的问题的方法是使用 集装箱 (或具有查找功能的上下文)

    解决方案:

    您的代码在以下位置失败:

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

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

    选择1: 使用一个简单的A-LA工厂模式来创建对象,但也要跟踪它们:

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

    然后你在食谱的一边做同样的事情 有问题的 代码固定:

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

    小心:可能是记忆泄露?

    有一件事你应该知道(这也是为什么你不应该使用 假人A-LA工厂 实施)是指 造物主 对象将保留对模型对象及其视图包装器的引用。因此,GC将无法从内存中清除它们。

    op-1a: 最可能的情况是,您的应用程序中已经有了一个控制器(或上下文),视图可以访问它。在这种情况下,而不是创建 A—LA工厂 ,我将把getorcreate方法移到这个上下文中。在这种情况下,当上下文消失(表单关闭)时,这些字典也将被取消引用,泄漏问题也将消失。

        4
  •  1
  •   Alex Martelli    17 年前

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

        5
  •  1
  •   JP Alioto    17 年前

    我想说 Factory Pattern .这样,您就可以依次构造每一个,然后将它们彼此添加,然后将它们全部返回工厂,以防被窥视。

        6
  •  1
  •   dviljoen    17 年前

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

    你基本上有相同的问题。解决方案可能和使用某种映射而不是列表集合一样简单。如果你得到的是一个多对多,那么你只需要创建一个列表的地图。

        7
  •  1
  •   Steven A. Lowe    17 年前

    选项:

    1. 执行成员资格测试,例如,在添加前检查栏是foo的成员
    2. 将多对多关系移动到自己的类中

    我认为后者更受欢迎——它在感情上更合理。

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

    编辑:考虑到原始问题中的代码,1将不起作用,因为无限递归发生在任何列表中添加任何内容之前。

    这种方法/问题有几个问题,可能是因为它被抽象到了近乎愚蠢的地步——很好地说明了编码问题,但对于解释最初的意图/目标却没有那么好的帮助:

    1. 包装器类实际上不包装任何东西或添加任何有用的行为;这使得很难理解为什么需要它们。
    2. 对于给定的结构,无法初始化构造函数中的列表 完全 因为每个包装器列表都会立即创建另一个包装器列表的新实例
    3. 即使您将初始化与构造分离,您仍然具有具有隐藏成员身份的循环依赖性(即包装器彼此引用,但将foo/bar元素隐藏在包含检查中,这并不重要,因为代码无论如何都无法向任何列表添加任何内容!)
    4. 直接关系方法可以工作,但需要搜索机制,并假定包装器将根据需要而不是事先创建,例如,具有搜索函数的数组或一对字典(例如Dictionary>、Dictionary>)可以用于映射,但可能不适合您的对象模型。

    结论

    我不认为结构 如上所述 会工作。不适用于DI,不适用于工厂,一点也不适用——因为包装器在隐藏子列表的同时相互引用。

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

    请用现实世界中的对象和期望的目标/意图在原始上下文中重述问题。

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

    补遗

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

    我没有使用WPF数据绑定——但是我略读了 this MSDN article -因此,以下内容可能有用,也可能不正确:

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

    您所拥有的结构是多对多关系的“反向索引”表示,这对于优化查找和依赖关系管理非常常见。它简化为一对一对多的关系。查看msdn文章中的gamesview模型示例-注意games属性

    ObservableCollection<Game>
    

    而不是

    ObservableCollection<GameWrapper>
    
        8
  •  0
  •   KnownIssues    17 年前

    所以,foo和bar是模型。foo是一个酒吧列表,bar是一个foo列表。如果我读得正确的话,你有两个物体,它们只是彼此的容器。A是所有B的集合,B是所有A的集合?这不是圆形的吗?它的定义是无限递归。现实世界中的案例是否包括更多的行为?也许这就是为什么人们难以解释解决方案的原因。

    我唯一的想法是,如果这确实是有意的,那么就使用静态类或使用静态变量来记录类是被创建了一次又一次的。

    推荐文章