英文:
Why is Linq Select making my new object a reference to the object I copied its constructor arguments from?
问题
Your first code snippet does not work as expected because the effectColor
variable inside the tuple is a reference to the original Outline
's effectColor
. When you change outline.effectColor
after storing it in the tuple, it modifies the original effectColor
in the tuple as well.
In your second approach, you are creating a new Color
object from the effectColor
and storing it in the tuple. This means that changes made to outline.effectColor
do not affect the stored tuple, allowing the code to work as intended.
The difference lies in how the tuples are created. In the first code snippet, the tuple contains a reference to the original effectColor
object, so changes to outline.effectColor
are reflected in the tuple. In the second snippet, you create a new Color
object, which is not affected by subsequent changes to outline.effectColor
.
Creating a new Color
object in the second approach ensures that the tuple holds a distinct copy of the color data, preventing unintended modifications.
英文:
I'm making a video game using Unity & C#. In the initialization function for one of the enemies, it uses C# Coroutines to
- Make its outlines transparent
- Wait for the loading animation to finish
- Return the outlines to their original color.
This was my first attempt:
IEnumerable<(Outline, Color)> outlines = GetComponentsInChildren<Outline>()
.Select(delegate (Outline outline)
{
float r = outline.effectColor.r;
float g = outline.effectColor.g;
float b = outline.effectColor.b;
float a = outline.effectColor.a;
return (outline, new Color(r, g, b, a));
});
foreach ((Outline outline, Color _) in outlines) {
Color transparent = Vector4.zero;
outline.effectColor = transparent;
}
yield return new WaitForSeconds(LOAD_TIME);
// ...
foreach ((Outline outline, Color effectColor) in outlines)
{
// Mysteriously, effectColor is assigned to Vector4.zero here.
// Setting effectColor to Color.cyan above also causes us to set it to cyan here.
outline.effectColor = effectColor;
}
But this does not work. Even though I created a copy of the effectColor
to put in my tuple, manipulating outline.effectColor
changes the value in the tuple. I checked this by assigning outline.effectColor
to cyan instead of transparent, to make sure we weren't losing the reference entirely.
The below code works, though, and I don't understand why. Shouldn't they be doing the same thing? Is there something fundamentally different about creating an object in the body of an anonymous function vs a for loop?
// ...
List<(Outline, Color)> outlines = new List<(Outline, Color)>();
foreach (Outline outline in GetComponentsInChildren<Outline>())
{
float r = outline.effectColor.r;
float g = outline.effectColor.g;
float b = outline.effectColor.b;
float a = outline.effectColor.a;
outlines.Add((outline, new Color(r, g, b, a)));
}
foreach ((Outline outline, Color _) in outlines) {
outline.effectColor = Vector4.zero;
}
yield return new WaitForSeconds(LOAD_TIME);
// ...
foreach ((Outline outline, Color effectColor) in outlines)
{
// effectColor is correctl assigned to its original color in this
// implementation
outline.effectColor = effectColor;
}
So far I've read some documentation to try and figure out whether something magical is going on under the hood, but I haven't been able to.
https://docs.unity3d.com/ScriptReference/Color.html
https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.select?view=net-7.0
答案1
得分: 2
I read the Linq Select documentation more closely and figured it out!
[Select] is implemented by using deferred execution. The immediate
return value is an object that stores all the information that is
required to perform the action. The query represented by this method
is not executed until the object is enumerated either by calling its
GetEnumerator method directly or by using foreach in Visual C# or For
Each in Visual Basic.
"Why is Linq Select making my new object a reference to the object I copied its constructor arguments from?" Because that's what it does, lol. I guess it stores the outline
as well as a function : Outine -> (Outline, Color)
and applies the function once it needs to use the tuple?
I appended .ToList() to my [outlines] declaration to cause the values to "lock in", which resulted in the behavior I expected.
英文:
I read the Linq Select documentation more closely and figured it out!
> [Select] is implemented by using deferred execution. The immediate
> return value is an object that stores all the information that is
> required to perform the action. The query represented by this method
> is not executed until the object is enumerated either by calling its
> GetEnumerator method directly or by using foreach in Visual C# or For
> Each in Visual Basic.
"Why is Linq Select making my new object a reference to the object I copied its constructor arguments from?" Because that's what it does, lol. I guess it stores the outline
as well as a function : Outine -> (Outline, Color)
and applies the function once it needs to use the tuple?
I appended .ToList() to my [outlines] declaration to cause the values to "lock in", which resulted in the behavior I expected.
答案2
得分: 1
The reason why the first version of your code is not behaving as you expect is because of deferred execution in LINQ.
当您使用LINQ方法如Select时,它不会立即执行委托函数中的代码。相反,它返回一个IEnumerable,该IEnumerable在枚举时执行代码。在您的情况下,这意味着直到您在foreach循环中枚举轮廓时,颜色对象才会被实际创建。
因此,颜色对象是在您已经将outline.effectColor设置为Vector4.zero之后创建的,因此您最终得到的是颜色始终为Vector4.zero的元组列表。
您的第二个代码版本之所以有效,是因为您手动创建了颜色对象并将其添加到列表中。这会在执行代码时立即发生,而不是在枚举列表时发生,因此您最终得到了原始颜色,与预期相符。
以下是您第一个代码片段的修改版本,应该能够正常工作:
var outlines = GetComponentsInChildren<Outline>()
.Select(outline => (outline, new Color(outline.effectColor.r, outline.effectColor.g, outline.effectColor.b, outline.effectColor.b, outline.effectColor.a)))
.ToList(); // ToList会强制立即执行
foreach ((Outline outline, Color _) in outlines)
{
outline.effectColor = Vector4.zero;
}
yield return new WaitForSeconds(LOAD_TIME);
foreach ((Outline outline, Color effectColor) in outlines)
{
outline.effectColor = effectColor;
}
在这个版本中,我在Select方法之后添加了一个ToList调用。这会强制立即执行委托函数,因此颜色对象将以原始颜色值创建。
英文:
The reason why the first version of your code is not behaving as you expect is because of deferred execution in LINQ.
When you use a LINQ method like Select, it doesn't immediately execute the code inside the delegate function. Instead, it returns an IEnumerable that will execute the code when it's enumerated. In your case, this means the color objects are not actually created until you enumerate outlines in your foreach loop.
Because of this, the color objects are created after you've already set outline.effectColor to Vector4.zero, so you end up with a list of tuples where the color is always Vector4.zero.
The second version of your code works because you're manually creating the color objects and adding them to a list. This happens immediately when you execute the code, not when you enumerate the list, so you end up with the original colors as expected.
Here is a modified version of your first code snippet that should work:
var outlines = GetComponentsInChildren<Outline>()
.Select(outline => (outline, new Color(outline.effectColor.r, outline.effectColor.g, outline.effectColor.b, outline.effectColor.b, outline.effectColor.a)))
.ToList(); // ToList forces immediate execution
foreach ((Outline outline, Color _) in outlines)
{
outline.effectColor = Vector4.zero;
}
yield return new WaitForSeconds(LOAD_TIME);
foreach ((Outline outline, Color effectColor) in outlines)
{
outline.effectColor = effectColor;
}
In this version, I've added a call to ToList after the Select method. This forces immediate execution of the delegate function, so the color objects are created with the original color values.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论