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

时间:2022-07-02 08:41:02

It feels like there must be some semi-simple solution to this, but I just can't figure it out.

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

Edit: The previous example showed the infinite loop more clearly, but this gives a bit more context. Check out the pre-edit for a quick overview of the problem.

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

The following 2 classes represent the View-Models of the Model View View-Model (MVVM) pattern.

以下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();
    }
}

I won't bother showing the Models (Recipe and Category) unless requested, but they basically take care of the business logic (for instance adding a recipe to a category will also add the other end of the link, i.e. if a category contains a recipe, then the recipe is also contained in that category) and basically dictate how things go. The ViewModels provide a nice interface for WPF databinding. That's the reason for the wrapper classes

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

Since the infinite loop is in the constructor and it's trying to create new objects, I can't just set a boolean flag to prevent this because neither object ever gets finished being constructed.

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

What I'm thinking is having (either as a singleton or passed in to the constructor or both) a Dictionary<Recipe, RecipeViewModel> and Dictionary<Category, CategoryViewModel> that will lazy-load the view models, but not create a new one if one already exists, but I haven't gotten around to trying to see if it'll work since it's getting late and I'm kinda tired of dealing with this for the past 6 hours or so.

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

No guarantee the code here will compile since I took a bunch of stuff out that was unrelated to the problem at hand.

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

8 个解决方案

#1


Man, my answer is not as cool as those DI ones. But...

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

In simplest terms, I think you must create your wrappers before you start relating them. Traverse your entire list of Foos, creating FooWrappers. Then traverse Bars and create BarWrappers. Then read the source Foos, adding appropriate BarWrapper references to the MyBarWrappers in the associated FooWrapper, and vice versa for Bars.

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

If you insist on both creating a wrapper for a Foo instance and immediately creating relationships to each of it's Bar instances, then you must "break" the cycle by marking which Foo you are working on, i.e. Foo_1, and let each of the BarWrapper instances know NOT to create yet another FooWrapper_1 instance inside it's MyFooWrappers collection. After all, you are, in fact, already creating the FooWrapper_1 higher up (or further down, as it were) the call stack.

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

Bottom line: as a matter of code sanity, the wrapper constructors should not be creating more wrappers. At the very most - it should only know/find that a single unique wrapper exists somewhere else for each Foo and Bar, and MAYBE create the wrapper ONLY if it doesn't find it elsewhere.

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

#2


Back to your original question (and the code). If what you want is to have a many-2-many relation that is automatically synchronized, then read on. The best place to look for a complex code that handles these cases is the source code of any ORM framework and it is very common problem for this domain of tools. I would look at the source code of nHibernate (https://nhibernate.svn.sourceforge.net/svnroot/nhibernate/trunk/nhibernate/) to see how it implements collections that handle both 1-N and M-N relations.

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

Simple something you can try is to create your own little collection class that just takes care of that. Below I removed your original wrapper classes and added a BiList collection, which is initialized with the object (the owner of the collection) and the name of the other side of the property to keep in sync with (works only for M-N, but 1-N would be simple to add). Of course, you would want to polish the code:

您可以尝试的简单方法是创建自己的小集合类,只需要处理它。下面我删除了你原来的包装类,并添加了一个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);
        }
    }
}

#3


First of all DI is not going to solve your problem, but one thing always related to DI which IS going to solve your problem is usage of a Container (or a Context with lookup capability)

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

Solution:

You code fails in these places:

你在这些地方编码失败:

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

The problem is caused by the fact that you create the wrapper (xxxViewModel) for an object, even if it already exists. Instead of creating a wrapper for the same object again, you need to check if a wrapper for this model already exists and use it instead. So you need a Container to keep track of all created objects. Your options are:

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

option-1: use a simple a-la Factory Pattern to create your objects, but also keep track of them:

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

Then you do the same thing on the side of Recipe and problematic code is fixed:

然后你在Recipe一边做同样的事情,修复了有问题的代码:

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

Beware: possible memory leaks?

注意:可能的内存泄漏?

One thing you should be aware of (and that is also why you should not use the dummy a-la factory implementation) is the fact that these creator objects will keep references to both the model objects and their View wrappers. Therefore the GC will not be able to clean them from the memory.

您应该注意的一件事(这也是您不应该使用虚拟a-la工厂实现的原因)是这些创建者对象将继续引用模型对象及其View包装器。因此GC将无法从内存中清除它们。

option-1a: most probably you have a Controller (or Context) in your application already, to which views have access to. In this case instead of creating those a-la factories, I would just move GetOrCreate methods to this context. In this case when the context is gone (the form is closed), also those dictionaries will be de-referenced and the leak issue is gone.

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

#4


I would recommend you get rid of the mutual dependency, for example through the Dependency Inversion principle, http://en.wikipedia.org/wiki/Dependency_inversion_principle -- have at least one of the two sides Foo and Bar (or their wrappers) depend on an abstract interface which the other side implements, rather than having two concrete classes directly depend on each other, which can easily produce circular-dependency and mutual-recursion nightmares like the one you're observing. Also, there are alternative ways to implement many-to-many relationships that may be worth considering (and may be easier to subject to inversion of dependency through the introduction of suitable interfaces).

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

#5


I am going to say Factory Pattern. That way, you can construct each in turn, then add them to each other, then return them all hidden from prying eyes by the factory.

我要说工厂模式。这样,您可以依次构造每个,然后将它们相互添加,然后将它们全部隐藏在工厂的窥探之外。

#6


This reminds me of the way serialization prevents infinite loops when objects contain other objects. It maps the hash code of each object to its byte array so when an object contains a reference to another object it: a) doesn't serialize the same object twice, and b) doesn't serialize itself into an infinite loop.

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

You have essentially the same issue. The solution could be just as simple as using some kind of map instead of list collection. If what you're getting at is a many-to-many then you just create a map of lists.

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

#7


options:

  1. implement a membership test, e.g. check bar-is-member-of-foo before adding
  2. 实施会员资格测试,例如在添加之前检查bar-is-member-of-foo

  3. move the many-to-many relationship to its own class
  4. 将多对多关系移动到自己的类

the latter is preferred, i think - it's more relationally sound

我认为后者是首选 - 它更具关系性

of course, with a foo-bar example we really don't know what the goal is, so your mileage may vary

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

EDIT: given the code in the original question, #1 will not work because the infinite recursion happens before anything is ever added to any list.

编辑:给定原始问题中的代码,#1将无法工作,因为无限递归发生在任何事物被添加到任何列表之前。

There are several problems with this approach/question, probably because it has been abstracted to the point of near silliness - good for illustrating the coding problem, no so good for explaining the original intent/goal:

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

  1. the wrapper classes don't actually wrap anything or add any useful behavior; this makes it difficult to see why they're needed
  2. 包装类实际上不包装任何东西或添加任何有用的行为;这使得很难理解他们为什么需要它们

  3. with the given structure, you cannot initialize the lists in the constructor at all because each wrapper list immediately creates a new instance of the other wrapper list
  4. 使用给定的结构,您根本无法初始化构造函数中的列表,因为每个包装器列表会立即创建另一个包装器列表的新实例

  5. even if you separate initialization from construction, you still have a cyclic dependency with hidden membership (i.e. the wrappers reference each other but hide the foo/bar elements from a contains check; which doesn't really matter because the code never gets to add anything to any list anyway!)
  6. 即使你将初始化与构造分开,你仍然具有隐藏成员资格的循环依赖(即包装器相互引用但隐藏了包含检查的foo / bar元素;这并不重要,因为代码永远不会添加任何东西反正任何名单!)

  7. a direct relational approach would work, but requires searching mechanisms and assumes that wrappers would be created as-needed instead of in advance, e.g. an array with search functions or a pair of dictionaries (e.g. Dictionary>, Dictionary>) would work for the mapping but might not fit your object model
  8. 直接关系方法可行,但需要搜索机制并假设将根据需要而不是提前创建包装器,例如,带有搜索功能的数组或一对词典(例如Dictionary>,Dictionary>)可用于映射但可能不适合您的对象模型

Conclusion

I don't think the structure as described will work. Not with DI, not with a factory, not at all - because the wrappers reference each other while hiding the sublists.

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

This structure hints at unstated incorrect assumptions, but with no context we can't ferret out what they might be.

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

Please restate the problem in the original context with real-world objects and desired goal/intent.

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

Or at least state what structure you think your sample code should produce. ;-)

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

Addendum

Thanks for the clarification, this makes the situation more comprehensible.

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

I have not worked with WPF databinding - but I have skimmed this MSDN article - so the following may or may not be helpful and/or correct:

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

  • I think the categories and recipes collections in the view-model classes are redundant
    • you already have the M:M information in the underlying category object, so why duplicate it in the view-model
    • 您已经在基础类别对象中拥有M:M信息,因此为什么要在视图模型中复制它

    • it looks like your collection-changed handlers will also cause infinite recursion
    • 看起来你的集合改变处理程序也会导致无限递归

    • the collection-changed handlers do not appear to update the underlying M:M information for the wrapped recipe/category
    • 集合更改的处理程序似乎不更新包装的配方/类别的基础M:M信息

  • 我认为视图模型类中的类别和配方集合是冗余的,你已经在底层类别对象中有M:M信息,所以为什么在视图模型中复制它看起来像你的集合改变处理程序也会导致无限递归集合更改的处理程序似乎不更新包装的配方/类别的基础M:M信息

  • I think the purpose of the view-model is to expose the underlying model data, not to indivudally wrap each of its components.
    • This seems redundant and a violation of encapsulation
    • 这似乎是多余的,并且违反了封装

    • It is also the source of your infinite-recursion problem
    • 它也是无限递归问题的根源

    • Naively, I would expect the ObservableCollection properties to merely return the underlying model's collections...
    • 天真的,我希望ObservableCollection属性只返回底层模型的集合......

  • 我认为视图模型的目的是公开底层模型数据,而不是单独包装其每个组件。这似乎是多余的并且违反了封装它也是你的无限递归问题的根源天真地,我希望ObservableCollection属性只返回底层模型的集合......

The structure you have is an "inverted index" representation of a many-to-many relationship, which is quite common for optimized lookups and dependency management. It reduces to a pair of one-to-many relationships. Look at the GamesViewModel example in the MSDN article - note that the Games property is just

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

ObservableCollection<Game>

and not

ObservableCollection<GameWrapper>

#8


So, Foo and Bar are the Models. Foo is a list of Bars and Bar is a list of Foos. If I'm reading that correctly you have two objects which are nothing but containers of each other. A is the set of all Bs and B is the set of all As? Isn't that circular by its very nature? It's infinite recursion by its very definition. Does the real world case include more behavior? Perhaps that's why people are having a difficult time explaining a solution.

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

My only thought is that if this is truly on purpose then to use static classes or to use a static variable to record that the classes had been created once and only once.

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

#1


Man, my answer is not as cool as those DI ones. But...

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

In simplest terms, I think you must create your wrappers before you start relating them. Traverse your entire list of Foos, creating FooWrappers. Then traverse Bars and create BarWrappers. Then read the source Foos, adding appropriate BarWrapper references to the MyBarWrappers in the associated FooWrapper, and vice versa for Bars.

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

If you insist on both creating a wrapper for a Foo instance and immediately creating relationships to each of it's Bar instances, then you must "break" the cycle by marking which Foo you are working on, i.e. Foo_1, and let each of the BarWrapper instances know NOT to create yet another FooWrapper_1 instance inside it's MyFooWrappers collection. After all, you are, in fact, already creating the FooWrapper_1 higher up (or further down, as it were) the call stack.

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

Bottom line: as a matter of code sanity, the wrapper constructors should not be creating more wrappers. At the very most - it should only know/find that a single unique wrapper exists somewhere else for each Foo and Bar, and MAYBE create the wrapper ONLY if it doesn't find it elsewhere.

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

#2


Back to your original question (and the code). If what you want is to have a many-2-many relation that is automatically synchronized, then read on. The best place to look for a complex code that handles these cases is the source code of any ORM framework and it is very common problem for this domain of tools. I would look at the source code of nHibernate (https://nhibernate.svn.sourceforge.net/svnroot/nhibernate/trunk/nhibernate/) to see how it implements collections that handle both 1-N and M-N relations.

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

Simple something you can try is to create your own little collection class that just takes care of that. Below I removed your original wrapper classes and added a BiList collection, which is initialized with the object (the owner of the collection) and the name of the other side of the property to keep in sync with (works only for M-N, but 1-N would be simple to add). Of course, you would want to polish the code:

您可以尝试的简单方法是创建自己的小集合类,只需要处理它。下面我删除了你原来的包装类,并添加了一个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);
        }
    }
}

#3


First of all DI is not going to solve your problem, but one thing always related to DI which IS going to solve your problem is usage of a Container (or a Context with lookup capability)

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

Solution:

You code fails in these places:

你在这些地方编码失败:

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

The problem is caused by the fact that you create the wrapper (xxxViewModel) for an object, even if it already exists. Instead of creating a wrapper for the same object again, you need to check if a wrapper for this model already exists and use it instead. So you need a Container to keep track of all created objects. Your options are:

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

option-1: use a simple a-la Factory Pattern to create your objects, but also keep track of them:

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

Then you do the same thing on the side of Recipe and problematic code is fixed:

然后你在Recipe一边做同样的事情,修复了有问题的代码:

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

Beware: possible memory leaks?

注意:可能的内存泄漏?

One thing you should be aware of (and that is also why you should not use the dummy a-la factory implementation) is the fact that these creator objects will keep references to both the model objects and their View wrappers. Therefore the GC will not be able to clean them from the memory.

您应该注意的一件事(这也是您不应该使用虚拟a-la工厂实现的原因)是这些创建者对象将继续引用模型对象及其View包装器。因此GC将无法从内存中清除它们。

option-1a: most probably you have a Controller (or Context) in your application already, to which views have access to. In this case instead of creating those a-la factories, I would just move GetOrCreate methods to this context. In this case when the context is gone (the form is closed), also those dictionaries will be de-referenced and the leak issue is gone.

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

#4


I would recommend you get rid of the mutual dependency, for example through the Dependency Inversion principle, http://en.wikipedia.org/wiki/Dependency_inversion_principle -- have at least one of the two sides Foo and Bar (or their wrappers) depend on an abstract interface which the other side implements, rather than having two concrete classes directly depend on each other, which can easily produce circular-dependency and mutual-recursion nightmares like the one you're observing. Also, there are alternative ways to implement many-to-many relationships that may be worth considering (and may be easier to subject to inversion of dependency through the introduction of suitable interfaces).

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

#5


I am going to say Factory Pattern. That way, you can construct each in turn, then add them to each other, then return them all hidden from prying eyes by the factory.

我要说工厂模式。这样,您可以依次构造每个,然后将它们相互添加,然后将它们全部隐藏在工厂的窥探之外。

#6


This reminds me of the way serialization prevents infinite loops when objects contain other objects. It maps the hash code of each object to its byte array so when an object contains a reference to another object it: a) doesn't serialize the same object twice, and b) doesn't serialize itself into an infinite loop.

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

You have essentially the same issue. The solution could be just as simple as using some kind of map instead of list collection. If what you're getting at is a many-to-many then you just create a map of lists.

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

#7


options:

  1. implement a membership test, e.g. check bar-is-member-of-foo before adding
  2. 实施会员资格测试,例如在添加之前检查bar-is-member-of-foo

  3. move the many-to-many relationship to its own class
  4. 将多对多关系移动到自己的类

the latter is preferred, i think - it's more relationally sound

我认为后者是首选 - 它更具关系性

of course, with a foo-bar example we really don't know what the goal is, so your mileage may vary

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

EDIT: given the code in the original question, #1 will not work because the infinite recursion happens before anything is ever added to any list.

编辑:给定原始问题中的代码,#1将无法工作,因为无限递归发生在任何事物被添加到任何列表之前。

There are several problems with this approach/question, probably because it has been abstracted to the point of near silliness - good for illustrating the coding problem, no so good for explaining the original intent/goal:

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

  1. the wrapper classes don't actually wrap anything or add any useful behavior; this makes it difficult to see why they're needed
  2. 包装类实际上不包装任何东西或添加任何有用的行为;这使得很难理解他们为什么需要它们

  3. with the given structure, you cannot initialize the lists in the constructor at all because each wrapper list immediately creates a new instance of the other wrapper list
  4. 使用给定的结构,您根本无法初始化构造函数中的列表,因为每个包装器列表会立即创建另一个包装器列表的新实例

  5. even if you separate initialization from construction, you still have a cyclic dependency with hidden membership (i.e. the wrappers reference each other but hide the foo/bar elements from a contains check; which doesn't really matter because the code never gets to add anything to any list anyway!)
  6. 即使你将初始化与构造分开,你仍然具有隐藏成员资格的循环依赖(即包装器相互引用但隐藏了包含检查的foo / bar元素;这并不重要,因为代码永远不会添加任何东西反正任何名单!)

  7. a direct relational approach would work, but requires searching mechanisms and assumes that wrappers would be created as-needed instead of in advance, e.g. an array with search functions or a pair of dictionaries (e.g. Dictionary>, Dictionary>) would work for the mapping but might not fit your object model
  8. 直接关系方法可行,但需要搜索机制并假设将根据需要而不是提前创建包装器,例如,带有搜索功能的数组或一对词典(例如Dictionary>,Dictionary>)可用于映射但可能不适合您的对象模型

Conclusion

I don't think the structure as described will work. Not with DI, not with a factory, not at all - because the wrappers reference each other while hiding the sublists.

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

This structure hints at unstated incorrect assumptions, but with no context we can't ferret out what they might be.

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

Please restate the problem in the original context with real-world objects and desired goal/intent.

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

Or at least state what structure you think your sample code should produce. ;-)

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

Addendum

Thanks for the clarification, this makes the situation more comprehensible.

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

I have not worked with WPF databinding - but I have skimmed this MSDN article - so the following may or may not be helpful and/or correct:

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

  • I think the categories and recipes collections in the view-model classes are redundant
    • you already have the M:M information in the underlying category object, so why duplicate it in the view-model
    • 您已经在基础类别对象中拥有M:M信息,因此为什么要在视图模型中复制它

    • it looks like your collection-changed handlers will also cause infinite recursion
    • 看起来你的集合改变处理程序也会导致无限递归

    • the collection-changed handlers do not appear to update the underlying M:M information for the wrapped recipe/category
    • 集合更改的处理程序似乎不更新包装的配方/类别的基础M:M信息

  • 我认为视图模型类中的类别和配方集合是冗余的,你已经在底层类别对象中有M:M信息,所以为什么在视图模型中复制它看起来像你的集合改变处理程序也会导致无限递归集合更改的处理程序似乎不更新包装的配方/类别的基础M:M信息

  • I think the purpose of the view-model is to expose the underlying model data, not to indivudally wrap each of its components.
    • This seems redundant and a violation of encapsulation
    • 这似乎是多余的,并且违反了封装

    • It is also the source of your infinite-recursion problem
    • 它也是无限递归问题的根源

    • Naively, I would expect the ObservableCollection properties to merely return the underlying model's collections...
    • 天真的,我希望ObservableCollection属性只返回底层模型的集合......

  • 我认为视图模型的目的是公开底层模型数据,而不是单独包装其每个组件。这似乎是多余的并且违反了封装它也是你的无限递归问题的根源天真地,我希望ObservableCollection属性只返回底层模型的集合......

The structure you have is an "inverted index" representation of a many-to-many relationship, which is quite common for optimized lookups and dependency management. It reduces to a pair of one-to-many relationships. Look at the GamesViewModel example in the MSDN article - note that the Games property is just

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

ObservableCollection<Game>

and not

ObservableCollection<GameWrapper>

#8


So, Foo and Bar are the Models. Foo is a list of Bars and Bar is a list of Foos. If I'm reading that correctly you have two objects which are nothing but containers of each other. A is the set of all Bs and B is the set of all As? Isn't that circular by its very nature? It's infinite recursion by its very definition. Does the real world case include more behavior? Perhaps that's why people are having a difficult time explaining a solution.

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

My only thought is that if this is truly on purpose then to use static classes or to use a static variable to record that the classes had been created once and only once.

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