如何避免这种无限循环?
-
05-09-2019 - |
题
感觉必须有一些半简单的解决方案,但我就是想不出来。
编辑:前面的示例更清楚地显示了无限循环,但这提供了更多背景信息。查看预编辑以快速了解问题。
以下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();
}
}
除非有要求,否则我不会费心显示模型(菜谱和类别),但它们基本上负责业务逻辑(例如,向类别添加菜谱也会添加链接的另一端,即如果一个类别包含一个菜谱,那么该菜谱也包含在该类别中)并且基本上决定了事情的进展。ViewModel 为 WPF 数据绑定提供了一个很好的接口。这就是包装类的原因
由于无限循环位于构造函数中并且它正在尝试创建新对象,因此我不能仅设置布尔标志来防止这种情况,因为这两个对象都没有完成构造。
我的想法是(作为单例或传递给构造函数或两者兼而有之) Dictionary<Recipe, RecipeViewModel>
和 Dictionary<Category, CategoryViewModel>
这将延迟加载视图模型,但如果已经存在,则不会创建一个新的视图模型,但我还没有抽出时间来尝试看看它是否能工作,因为已经很晚了,我有点厌倦了处理这个问题过去 6 个小时左右。
不能保证这里的代码能够编译,因为我取出了一堆与当前问题无关的东西。
解决方案
男人,我的答案是不如那些DI作为清凉。但...
在简单地说,我觉得你开始与他们之前,您必须创建包装。遍历FOOS的完整清单,创建FooWrappers。然后横杆,创造BarWrappers。然后读取源FOOS,在相关联的FooWrapper增加了MyBarWrappers适当BarWrapper参考文献,以及用于酒吧反之亦然。
如果你坚持既创造了一个Foo实例的包装,并立即建立关系,以每次酒吧实例,则必须“破发”的标记周期,这的Foo您正在使用的,即FOO_1,并让每个在BarWrapper情况下,不知道创造另一个FooWrapper_1实例内是MyFooWrappers集合。毕竟,你是,事实上,已经创造了FooWrapper_1上涨(或进一步下跌,因为它是)调用堆栈。
底线:作为代码理智的问题,包装构造不应被创造更多的包装。在很大部分 - 它应该只知道/发现一个独特的包装存在别的地方各Foo和酒吧,也许创建包装,只有当它没有在别的地方找到
。其他提示
回到你原来的问题(和代码)。如果你想要的是有许多-2-许多关系是自动同步,然后阅读。 找一个复杂的代码,处理这些情况的最佳位置是任何ORM框架的源代码,它是工具,这个领域非常普遍的问题。我想看看NHibernate的源代码( HTTPS://nhibernate.svn .sourceforge.net / svnroot / NHibernate的/中继/ NHibernate的/ )看看它是如何实现了同时处理1-N和MN关系集合。
简单的东西你可以尝试是创建你自己的小集合类只是需要的照顾。下面我删除了原来的包装类,并添加了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);
}
}
}
所有的 DI 的是不会解决你的问题,但有一点始终涉及到的 DI 的这是会解决你的问题首先是一个使用的容器(或者与查找能力的上下文)
<强>解决方案:强>
您的代码在这些地方失败:
var catVM = new CategoryViewModel(cat); //Causes infinite loop
...
var recipeVM = new RecipeViewModel(item); //Causes infinite loop
问题是由所创建的某个对象的包装物(xxxViewModel),即使它已经存在这样的事实引起的。相反,再次创造了同一个对象的包装,你需要检查,如果该模型的包装已经存在,并且用它来代替。所以,你需要一个容器来跟踪所有创建的对象。您的选项是:
选项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];
}
}
然后,你做方侧的同样的事情,问题代码是固定的:
// OLD: Causes infinite loop
//var catVM = new CategoryViewModel(cat);
// NEW: Works
var catVM = CategoryViewModelFactory.GetOrCreate(cat);
<强>小心:可能的内存泄漏强>
有一件事你应该知道的(这也是为什么你不应该使用的假一拉工厂的实施)的事实是,这些的创作者的对象将继续引用这两个模型对象及其查看包装。因此,GC将无法从存储器清除它们。
选项-1A:最有可能你有你的应用程序已经是一个控制器(或上下文),对此观点有机会获得。在这种情况下,而不是创造者的一拉工厂的,我也只是移动GetOrCreate方法来此背景下。在这种情况下当所述上下文被去(窗体关闭),也这些字典将被解引用和泄漏问题也没有了。
我会建议你通过依赖倒置原则摆脱相互依存的,例如, HTTP: //en.wikipedia.org/wiki/Dependency_inversion_principle - 具有双方Foo和Bar(或它们的包装)中的至少一个依赖于一个抽象的接口在其上的另一侧工具,而不是具有两个具体类直接依赖于对方,这很容易产生循环依赖性和相互递归恶梦像你观察一个。而且,存在实现许多一对多关系,这可能是值得考虑的(并且可以通过引入合适的接口更容易受到依赖的反转)。
的替代方式我要说工厂模式。这样,你可以依次构建每个,然后将它们彼此相加,然后返回他们所有隐藏的由工厂窥视。
此提醒我的序列化的方式可以防止无限循环时对象包含其他对象。它映射每个对象到其字节数组的哈希码,因此,在对象包含到另一个对象的引用:a)不序列相同的对象两次,和b)不本身序列化为一个无限循环
您有基本相同的问题。该解决方案可就像使用某种形式的地图,而不是名单收集的那样简单。如果有什么你在说是多到多,那么你只需要创建一个映射列表中。
选项:
- 实施成员资格测试,例如添加之前检查 bar-is-member-of-foo
- 将多对多关系移至其自己的类中
我认为后者是首选 - 它更相关
当然,对于 foo-bar 的例子,我们真的不知道目标是什么,所以你的里程可能会有所不同
编辑:给定原始问题中的代码,#1 将不起作用,因为无限递归发生在将任何内容添加到任何列表之前。
这种方法/问题存在几个问题,可能是因为它已经被抽象到近乎愚蠢的地步 - 适合说明编码问题,但不太适合解释最初的意图/目标:
- 包装类实际上并不包装任何东西或添加任何有用的行为;这让人很难理解为什么需要它们
- 使用给定的结构,您无法在构造函数中初始化列表 根本不 因为每个包装器列表立即创建另一个包装器列表的新实例
- 即使您将初始化与构造分开,您仍然具有隐藏成员资格的循环依赖关系(即包装器相互引用,但在包含检查中隐藏 foo/bar 元素;这并不重要,因为代码永远不会向任何列表添加任何内容!)
- 直接关系方法可行,但需要搜索机制并假设包装器将根据需要而不是提前创建,例如具有搜索功能的数组或一对字典(例如Dictionary>, Dictionary>) 适用于映射,但可能不适合您的对象模型
结论
我不认为结构 如上所述 将工作。DI 不行,工厂不行,根本不行——因为包装器在隐藏子列表时互相引用。
这种结构暗示了未阐明的错误假设,但在没有上下文的情况下,我们无法找出它们可能是什么。
请在原始上下文中使用现实世界的对象和期望的目标/意图重述问题。
或者至少说明您认为示例代码的结构 应该 生产。;-)
附录
感谢您的澄清,这使情况更容易理解。
我没有使用过 WPF 数据绑定 - 但我浏览过 这篇 MSDN 文章 - 因此以下内容可能有帮助和/或正确,也可能没有帮助:
- 我认为视图模型类中的类别和食谱集合是多余的
- 您已经在基础类别对象中拥有 M:M 信息,那么为什么要在视图模型中复制它
- 看起来你的集合更改处理程序也会导致无限递归
- 集合更改的处理程序似乎不会更新包装的配方/类别的基础 M:M 信息
- 我认为视图模型的目的是公开底层模型数据,而不是单独包装其每个组件。
- 这似乎是多余的并且违反了封装
- 这也是无限递归问题的根源
- 天真地,我希望 ObservableCollection 属性仅返回底层模型的集合......
您拥有的结构是多对多关系的“倒排索引”表示,这对于优化查找和依赖关系管理来说很常见。它简化为一对一对多的关系。查看 MSDN 文章中的 GamesViewModel 示例 - 请注意,Games 属性只是
ObservableCollection<Game>
并不是
ObservableCollection<GameWrapper>
所以,Foo和Bar是模型。 foo是酒吧的列表,酒吧是FOOS的列表。如果我读取正确你有两个对象这不过是对方的容器。 A是集所有烧烤和B是集合所有作为?那是一个循环就其本质?这是由于其本身的定义无限递归。是否真实世界的案例包括更多的行为?也许这就是为什么人们有困难的时候解释的解决方案。
我唯一的想法是,如果这是真正的目的,然后使用静态类或使用一个静态变量来记录类已经创建一次,只有一次。