Сокрытие WeakReference в C#
Недавно узнал о таком способе защиты от циклических зависимостей, как WeakReference. Очень удобный инструмент, однако для того, чтобы его внедрить к себе, мне пришлось везде переписывать свои классы в местах, где используются объекты с циклическими зависимостями, как WeakReference, и при этом ещё и каждый раз дополнять код тем, что я сначала вытаскиваю объект из WeakReference, а потом уже с ним работаю.
Есть ли какой-то способ "сокрытия" логики WeakReference, чтобы слабая ссылка на объект вела себя как сам объект?
(например, было: fooRefList.Select(a => a.TryGetTarget(out var fooRef) ? fooRef.Retrieve() : null) - стало: fooRefList.Select(a => a.Retrieve()) )
Я уже пробовал обернуть объекты в контейнеры, но всё в любом случае кончилось тем, что плодились сущности, а смысла не добавлялось. Хочется всё-таки, чтобы компилятор просто воспринимал ссылку как ссылку, неважно, слабая она или сильная.
UPD: Вкратце о том, чего я добиваюсь: у меня есть архитектура, схожая с ориентированным графом - есть 4 сущности, наследующие один интерфейс, и обладающие свойствами Parents и Children. Цикличность ссылок возникает из-за того, что если у одной сущности содержится объект в Parents, то и у Parent-объекта в Children содержится ссылка на искомую сущность. Поэтому вместо сильных ссылок в Parents и Children содержатся WeakReference от этих объектов. А чтобы сборщик мусора не собирал автоматически всю систему после построения, я создал класс с коллекцией всех сильных ссылок (которые должны быть единственными сильными ссылками в системе), и когда объект уничтожается, очищается сильная ссылка из этого пула, а сборщик сам собирает сущность.
Вот один из классов, который содержится в этом графе.
public sealed class Edge: DefaultMethodsMeshComponent, ParentOf<Vertex>, ChildOf<Polygon>, ChildOf<ComplexMesh>
{
private Pair<WeakReference<Vertex>> vertices = new();
public Pair<WeakReference<Vertex>> Vertices => vertices;
private Pair<WeakReference<Polygon>> parents = new Pair<WeakReference<Polygon>>();
public Pair<WeakReference<Polygon>> Parents => parents;
public WeakReference<Vertex> Other(Vertex v) => Other(new WeakReference<Vertex>(v));
public WeakReference<Vertex> Other(WeakReference<Vertex> v) => vertices.Other(v);
public WeakReference<Polygon> Other(Polygon p) => Other(new WeakReference<Polygon>(p));
public WeakReference<Polygon> Other(WeakReference<Polygon> p) => parents.Other(p);
public Edge(Vertex v1, Vertex v2) : base()
{
vertices = new(new WeakReference<Vertex>(v1), new WeakReference<Vertex>(v2));
v1.AddParent(new WeakReference<Edge>(this));
v2.AddParent(new WeakReference<Edge>(this));
}
public WeakReference<Vertex> GetCommonVertex(Edge edge)=> GetCommonVertex(new WeakReference<Edge>(edge));
public WeakReference<Vertex> GetCommonVertex(WeakReference<Edge> edgeRef)
{
var ref_comp = new WeakReferenceEqualityComparer<Vertex>();
if (!edgeRef.TryGetTarget(out var edge))
return null;
return edge.vertices.FirstOrDefault(e =>
ref_comp.Equals(e, vertices.v1) || ref_comp.Equals(e, vertices.v2));
}
public string ToString()
{
vertices.v1.TryGetTarget(out var tv1);
vertices.v2.TryGetTarget(out var tv2);
return $"[start: {tv1.ToString().Split(' ')[0]}, end: {tv2.ToString().Split(' ')[0]}]";
}
#region IMeshComponent methods
public override void Accept(IVisitor visitor)
{
visitor.Visit(this);
foreach (var v in vertices)
{
if(v.TryGetTarget(out var tv))
tv.Accept(visitor);
}
}
public override T Copy<T>()
{
vertices.v1.TryGetTarget(out var tv1);
vertices.v2.TryGetTarget(out var tv2);
return new Edge
(
tv1.CopyComponent<Vertex>(),
tv2.CopyComponent<Vertex>()
) as T;
}
public override IEnumerable<WeakReference<IMeshComponent>> GetDirectParents() => parents
.SelectMany(a => a
.TryGetTarget(out var t) ?
new[] { new WeakReference<IMeshComponent>(t) } :
Enumerable.Empty<WeakReference<IMeshComponent>>());
public override IEnumerable<WeakReference<IMeshComponent>> GetDirectChildren() => vertices
.SelectMany(a => a
.TryGetTarget(out var t) ?
new[] { new WeakReference<IMeshComponent>(t) } :
Enumerable.Empty<WeakReference<IMeshComponent>>());
public override IEnumerable<WeakReference<T>> AddParent<T>(WeakReference<T> parent)
{
parent.TryGetTarget(out var t);
if (t is not Polygon poly)
throw new ArgumentException("Expected Polygon");
var eparent = new WeakReference<Polygon>(poly);
if (!Enumerable.Contains
(parents,
eparent,
new WeakReferenceEqualityComparer<Polygon>()))
parents.Add(eparent);
return parents.Cast<WeakReference<T>>();
}
protected override bool RemoveParentUnchecked<T>(WeakReference<T> parent) =>
parent.TryGetTarget(out var tp) ? parents.Remove(tp as Polygon) : false;
public override void ClearParents()
{
foreach (var p in parents
.SelectMany(a => a.TryGetTarget(out var t) ?
new[] { t } :
Enumerable.Empty<Polygon>()))
p.RemoveChild(this);
parents.RemoveAll(a => true);
}
protected override bool RemoveChildUnchecked<T>(WeakReference<T> child) =>
child.TryGetTarget(out var tc) ? vertices.Remove(tc as Vertex) : false;
#endregion
}
Вот где у меня случился основной затык: безответственно пользуясь слабыми ссылками, у меня не получилось изначально очистить объект класса Polygon в методе Split(), так как его ссылка протащилась снаружи. Поэтому сейчас метод выглядит вот так:
public static WeakReference<Polygon>[] Split(WeakReference<Polygon> original, WeakReference<Vertex> v)
{
WeakReference<ComplexMesh> mesh;
WeakReference<Edge> edgeWithV;
WeakReference<Vertex>[] sorted;
WeakReference<Vertex> opp;
Vector3 centroid;
Plane plane;
{
original.TryGetTarget(out var toriginal);
centroid = toriginal.CentroidPos;
mesh = toriginal.parent;
plane = toriginal.plane;
sorted = toriginal.VerticesSorted.ToArray();
edgeWithV = toriginal.Edges.FirstOrDefault(e =>
{
e.TryGetTarget(out var te);
return Enumerable.Contains(te.Vertices,
v,
new WeakReferenceEqualityComparer<Vertex>());
});
opp = toriginal.OppositeToEdge(edgeWithV);
toriginal.DestroyComponent();
}
if (edgeWithV == null)
{
var verts = sorted.SelectMany(a => a.TryGetTarget(out var tv) ? new[] { tv } : Enumerable.Empty<Vertex>()).ToArray();
v.TryGetTarget(out var new_target);
mesh.TryGetTarget(out var tm);
return new[]
{
tm.CreatePolygon((verts[0], verts[1], new_target)),
tm.CreatePolygon((verts[1], verts[2], new_target)),
tm.CreatePolygon((verts[2], verts[0], new_target))
};
}
else
{
edgeWithV.TryGetTarget(out var tedgeWithV);
(Vertex, int)[] old_verts = new (Vertex, int)[2];
tedgeWithV.Vertices[0].TryGetTarget(out old_verts[0].Item1);
tedgeWithV.Vertices[1].TryGetTarget(out old_verts[1].Item1);
old_verts[0].Item2 = Array.FindIndex(sorted, a =>
new WeakReferenceEqualityComparer<Vertex>()
.Equals(tedgeWithV.Vertices[0], a));
old_verts[1].Item2 = Array.FindIndex(sorted, a =>
new WeakReferenceEqualityComparer<Vertex>()
.Equals(tedgeWithV.Vertices[1], a));
old_verts = old_verts.OrderBy(a => a.Item2).ToArray();
mesh.TryGetTarget(out var tm);
v.TryGetTarget(out var new_target);
opp.TryGetTarget(out var opp_target);
var sortedVertices = new[]
{
new [] {old_verts[0].Item1, new_target, opp_target}
.OrderBy(a => plane.GetAngleValue(centroid, a))
.ToList(),
new [] {new_target, old_verts[1].Item1, opp_target}
.OrderBy(a => plane.GetAngleValue(centroid, a))
.ToList(),
};
var result = new List<WeakReference<Polygon>>
{
tm.CreatePolygon((sortedVertices[0][0], sortedVertices[0][1], sortedVertices[0][2])),
tm.CreatePolygon((sortedVertices[1][0], sortedVertices[1][1], sortedVertices[1][2]))
};
return result.ToArray();
}
}
Однако, как видно, блок кода просто странный, громоздкий и приходится вручную каждый раз вытаскивать данные. При этом, множество методов Generic, например, метод в базовом классе:
private static void CollectRelatedComponentsRecursive<T>(IMeshComponent c, HashSet<WeakReference<T>> result)
where T : class, IMeshComponent
{
var visited = new ConcurrentDictionary<WeakReference<IMeshComponent>, byte>();
var resultSet = new ConcurrentDictionary<WeakReference<T>, byte>();
void Traverse(WeakReference<IMeshComponent> c, int depth)
{
if (depth > 4 || !visited.TryAdd(c, 0))
return;
if (c.TryGetTarget(out var t) && t is T match)
resultSet.TryAdd(new WeakReference<T>(match), 0);
if (t is ParentOf<T>)
Parallel.ForEach(t.GetDirectChildren(), child =>
{
Traverse(child, depth + 1);
});
else if (t is ChildOf<T>)
Parallel.ForEach(t.GetDirectParents(), parent =>
{
Traverse(parent, depth + 1);
});
else if (t is T)
{
// do nothing more — terminal case
}
else if (t != null || !t.Equals(default(T)))
{
throw new System.Exception("Hierarchy ruined!");
}
}
Traverse(new WeakReference<IMeshComponent>(c), 0);
foreach (var kvp in resultSet)
result.Add(kvp.Key);
}
Поэтому даже если я попробую добавить DTO с explicit cast'ом в качестве конструктора такого объекта, сложно будет обернуть всё анонимными делегатами, и нет смысла покрывать функциями, так как это нарушает DRY.
Ответы (1 шт):
Викреф от рефа отличается только тем, что позволяет сборщику забирать объект, когда на него нет сильных ссылок. Это нечто похожее на "бесплатное" кеширование, не более.
Викреф никак не поможет с циклическими ссылками, например если 2 объекта находятся в викрефах, но сами друг на друга имеют сильные ссылки, когда-нибудь сборщик может и сообразит их собрать, но это будет непростой задачей, не усложняйте ему работу.
Викреф несовсем бесплатный по производительности, и чтобы с ним работать, всё равно добавляются какие-то обвязки, чтобы логика могла этот викреф переваривать, например пересоздавать объект, если викреф потерял свой объект.
Поэтому хорошенько оцените, есть ли от этого прямая выгода, протестируйте.
Для начала, избавьтесь от списков в пользу массивов там, где вам не требуется динамическое изменение коллекции. Избавьтесь также от излишних копировпний данных, которые выполняются через ToList() или ToArray(). Вот это реально может помочь высвободить памяти.
Например
var result = new WeakReference<Polygon>[]
{
tm.CreatePolygon((sortedVertices[0][0], sortedVertices[0][1], sortedVertices[0][2])),
tm.CreatePolygon((sortedVertices[1][0], sortedVertices[1][1], sortedVertices[1][2]))
};
return result;
Также я бы не советовал объявлять переменные заранее.
WeakReference<ComplexMesh> mesh;
WeakReference<Edge> edgeWithV;
WeakReference<Vertex>[] sorted;
WeakReference<Vertex> opp;
Vector3 centroid;
Plane plane;
Объявляйте ровно там, где они нужны при первом присваивании, если явно не требуется обратное. Это позволит лучше контролировать их зону видимости, а компилятору лучше оптимизировать код.
И последний момент: выясните, где лучше возможно стоит использовать значимые типы вместо ссылочных. Но с этим осторожнее, это может сделать в некоторых случаях хуже.