英文:
convert action<> lambda to full delegate
问题
以下是您要翻译的部分:
我有以下使用 lambda 的代码:
this.Notify(action: listener => listener.OnAccelerometersChanged(x, y, z));
action 参数的类型是 `Action<IMyInterface>`。
我想将这个 lambda 转换为一个完整的委托,因为在 Xamarin.iOS 下,由于这个方法经常被调用,lambda 每次都会被编译,导致内存中的对象数量不断增加。
所以我想摆脱 lambda,改为一个只编译一次的方法。
这是可能的吗?
它的签名是什么?
编辑:使用 'full delegate' 这个词汇来自于 [这个视频][1] 的摘要。
编辑 2:这是方法:
private void NotifyNewAccelerometerValues(double x, double y, double z)
{
this.Notify(action: listener => listener.OnAccelerometersChanged(x, y, z));
...
}
编辑 3:这是整个类:
// 请注意,由于篇幅较长,这里省略了类的完整代码,只提供了类的名称和一些关键信息。
AccelerometerService : IOSServiceNotifier<IAccelerometerListener>, IAccelerometerService
编辑 4:这是 `Notify` 方法的实现:
protected void Notify(Action<TServiceListener> action)
{
lock (this.Locker)
{
// 移除了用于避免内存泄漏的 RemoveAll 中使用的谓词
foreach (TServiceListener listener in this.Listeners)
{
if (listener == null)
{
this.listeners.Remove(listener);
}
}
// 移除了 LINQ 表达式以避免泄漏
foreach (TServiceListener listener in this.listeners)
{
if (listener == null)
{
continue;
}
// 使用缓存以避免每次创建字符串
if (!listenersNameCache.TryGetValue(listener, out string name))
{
name = listener.GetType().Name;
this.listenersNameCache.Add(listener, name);
}
string errorMessage = string.Format(provider: CultureInfo.InvariantCulture, format: "Can't notify service update to listener {0}", args: name);
// 避免在调用 ManageExpession 时使用 lambda 表达式,通过复制代码来实现
try
{
action(obj: listener);
}
catch (Exception exception)
{
if (!string.IsNullOrWhiteSpace(value: errorMessage))
{
this.logger.WriteException(
exception: exception,
severity: SeverityLevel.Error,
logMessage: errorMessage);
}
else
{
this.logger.FormatException(
exception: exception,
severity: SeverityLevel.Error,
logMessageFormat: "An error occurs during call {0}",
args: nameof(this.Notify));
}
}
}
}
}
<details>
<summary>英文:</summary>
I have the following code that uses a lambda:
this.Notify(action: listener => listener.OnAccelerometersChanged(x, y, z));
action parameter is of type `Action<IMyInterface>`.
I would like to convert this lambda to a full delegate because under Xamarin.iOS, as this method is called very often, the lambda is compiled every time and the number of objects in memory increment endlessly.
So I would like to get rid of the lambda in favor of a method that is compiled once.
Is it possible?
What would be its signature?
EDIT: The use of 'full delegate' comes from the takeaways of [this video][1].
EDIT 2: Here is the method:
private void NotifyNewAccelerometerValues(double x, double y, double z)
{
this.Notify(action: listener => listener.OnAccelerometersChanged(x, y, z));
...
}
EDIT 3: Here is the whole class:
using RTE.Technologies.CommonServices.Services;
using RTE.Technologies.SystemTools.Log;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace RTE.Technologies.CommonServices
{
/// <summary>
/// This is the IOS implementation of the <see cref="IAccelerometerService"/>.
/// </summary>
public class AccelerometerService : IOSServiceNotifier<IAccelerometerListener>, IAccelerometerService
{
/// <summary> The <see cref="AccelerometerListener"/> instance. </summary>
private readonly AccelerometerListener accelerometerListener;
/// <summary> The <see cref="AutoResetEvent"/> for stop detection. </summary>
private readonly AutoResetEvent stopDetectionEvent = new AutoResetEvent(initialState: false);
/// <summary> The background <see cref="Task"/> for stop detection. </summary>
private readonly Task accelerometerServiceTask;
/// <summary> A value indicating whether if the waiting operation has been aborted. </summary>
private volatile bool abort;
/// <summary> The duration threshold. </summary>
private TimeSpan durationThreshold = TimeSpan.FromSeconds(value: 30d);
/// <summary> The movement threshold. </summary>
private double movementThreshold = 1d;
/// <summary> A value indicating whether if a parameter has changed. </summary>
private volatile bool parameterHasChanged;
/// <summary> A value indicating whether if a stop is waiting. </summary>
private volatile bool waitingStop;
/// <summary> A value indicating whether if a move has been detected. </summary>
private volatile bool isMoving = true;
public AccelerometerService(Logger logger) : base(logger)
{
this.accelerometerListener = new AccelerometerListener(service: this);
this.accelerometerServiceTask = Task.Factory.StartNew(action: this.CheckAccelerometerStopTimeout, creationOptions: TaskCreationOptions.LongRunning | TaskCreationOptions.DenyChildAttach);
}
/// <summary>
/// Adds or removes handler on <see cref="OnMoveDetectedEvent"/>.
/// </summary>
public event EventHandler<MoveDetectionEventArgs> OnMoveDetected
{
add { this.OnMoveDetectedEvent += value; }
remove { this.OnMoveDetectedEvent -= value; }
}
/// <summary>
/// Adds or removes handler on <see cref="OnAccelerationChangedEvent"/>.
/// </summary>
public event EventHandler<AccelerometerValuesEventArgs> OnAccelerationChanged
{
add { this.OnAccelerationChangedEvent += value; }
remove { this.OnAccelerationChangedEvent -= value; }
}
/// <summary>
/// Occurs when a move has been detected.
/// </summary>
private event EventHandler<MoveDetectionEventArgs> OnMoveDetectedEvent;
/// <summary>
/// Occurs when acceleration has changed.
/// </summary>
private event EventHandler<AccelerometerValuesEventArgs> OnAccelerationChangedEvent;
/// <summary>
/// Gets or sets the movement threshold.
/// </summary>
public double MovementThreshold
{
get
{
return this.movementThreshold;
}
set
{
if (Math.Abs(value: value - this.movementThreshold) > 0.00001d)
{
this.parameterHasChanged = true;
this.movementThreshold = value;
this.stopDetectionEvent.Set();
}
}
}
/// <summary>
/// Gets or sets the duration threshold.
/// </summary>
public TimeSpan DurationThreshold
{
get
{
return this.durationThreshold;
}
set
{
if (this.durationThreshold != value)
{
this.parameterHasChanged = true;
this.durationThreshold = value;
this.stopDetectionEvent.Set();
}
}
}
/// <summary>
/// Updates acceleration value in each axis.
/// </summary>
/// <param name="x">The acceleration on X.</param>
/// <param name="y">The acceleration on Y.</param>
/// <param name="z">The acceleration on Z.</param>
internal void UpdateValues(double x, double y, double z)
{
this.CheckMovement(x, y, z);
this.NotifyNewAccelerometerValues(x, y, z);
}
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">True if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (!this.IsDisposed)
{
this.abort = true;
if (disposing)
{
this.stopDetectionEvent.Set();
this.accelerometerServiceTask.Wait(timeout: TimeSpan.FromSeconds(value: 20d));
this.stopDetectionEvent.Dispose();
this.accelerometerListener?.Dispose();
}
}
base.Dispose(disposing: disposing);
}
/// <summary>
/// Notifies that moving state has changed.
/// </summary>
/// <param name="moving"></param>
private void OnMovingChanged(bool moving)
{
if (moving != this.isMoving)
{
this.isMoving = moving;
this.Notify(action: MovingAction(moving));
this.OnMoveDetectedEvent?.Invoke(sender: this, e: new MoveDetectionEventArgs(isMoving: moving));
}
}
private static Action<IAccelerometerListener> MovingAction(bool moving)
{
return listener => listener.OnMovingChanged(moving);
}
/// <summary>
/// Notifies the accelerometers new values.
/// </summary>
/// <param name="x">The acceleration on X.</param>
/// <param name="y">The acceleration on Y.</param>
/// <param name="z">The acceleration on Z.</param>
private void NotifyNewAccelerometerValues(double x, double y, double z)
{
this.Notify(action: listener => listener.OnAccelerometersChanged(x, y, z));
this.OnAccelerationChangedEvent?.Invoke(sender: this, e: new AccelerometerValuesEventArgs(x: x, y: y, z: z));
}
/// <summary>
/// Checks movement detection.
/// </summary>
/// <param name="x">The accelerometer value on X.</param>
/// <param name="y">The accelerometer value on Y.</param>
/// <param name="z">The accelerometer value on Z.</param>
private void CheckMovement(double x, double y, double z)
{
double vector = Math.Sqrt(d: (x * x) + (y * y) + (z * z));
if (vector > 10 - this.movementThreshold && vector < 10 + this.movementThreshold)
{
// Accelerometers don't detect movement.
if (this.isMoving && !this.waitingStop)
{
// Starts waiting accelerometers timeout.
this.waitingStop = true;
this.stopDetectionEvent.Set();
}
}
else
{
// Accelerometers detect movement.
if (this.waitingStop)
{
this.waitingStop = false;
this.stopDetectionEvent.Set();
}
if (!this.isMoving)
{
// Stops waiting accelerometers timeout.
this.Logger.Write(logMessage: "Accelerometers detect a movement");
this.OnMovingChanged(moving: true);
}
}
}
/// <summary>
/// Checks for stop in background.
/// </summary>
private void CheckAccelerometerStopTimeout()
{
this.Logger.Write(logMessage: "Start background stop checking");
TimeSpan timeout = Timeout.InfiniteTimeSpan;
while (!this.abort)
{
bool fromTimeout = !this.stopDetectionEvent.WaitOne(timeout: timeout);
if (this.abort)
{
return;
}
if (this.parameterHasChanged)
{
this.Logger.Write(logMessage: "Stop detection parameters have changed. Restart detection");
this.parameterHasChanged = this.waitingStop = false;
this.OnMovingChanged(moving: true);
timeout = this.durationThreshold;
}
else if (!fromTimeout)
{
// The event has been set. Change timeout value.
timeout = this.waitingStop ? this.durationThreshold : Timeout.InfiniteTimeSpan;
}
else
{
// A timeout occurs. Check timeout reason.
if (this.isMoving && this.waitingStop)
{
// The accelerometer timeout occurs.
this.Logger.Write(logMessage: "The accelerometer detects a stop.");
this.waitingStop = false;
this.OnMovingChanged(moving: false);
timeout = TimeSpan.FromSeconds(value: this.durationThreshold.TotalSeconds * 5);
}
else
{
timeout = Timeout.InfiniteTimeSpan;
}
}
}
}
}
}
EDIT 4 : Here is the `Notify` implementation
protected void Notify(Action<TServiceListener> action)
{
lock (this.Locker)
{
// Removed predicate used with RemoveAll to avoid memory leaks
foreach (TServiceListener listener in this.Listeners)
{
if (listener == null)
{
this.listeners.Remove(listener);
}
}
// Removed the linq expression to avoid leaks
foreach (TServiceListener listener in this.listeners)
{
if (listener == null)
{
continue;
}
// using a cache to avoid string being created everytime.
if (!listenersNameCache.TryGetValue(listener, out string name))
{
name = listener.GetType().Name;
this.listenersNameCache.Add(listener, name);
}
string errorMessage = string.Format(provider: CultureInfo.InvariantCulture, format: "Can't notify service update to listener {0}", args: name);
// Avoid using lambda expression in a call to ManageExpession by ducplicating the code.
try
{
action(obj: listener);
}
catch (Exception exception)
{
if (!string.IsNullOrWhiteSpace(value: errorMessage))
{
this.logger.WriteException(
exception: exception,
severity: SeverityLevel.Error,
logMessage: errorMessage);
}
else
{
this.logger.FormatException(
exception: exception,
severity: SeverityLevel.Error,
logMessageFormat: "An error occurs during call {0}",
args: nameof(this.Notify));
}
}
}
}
}
[1]: https://learn.microsoft.com/en-us/events/xamarin-xamarin-developer-summit-2019/your-xamarin-application-is-probably-leaking-memory-and-you-dont-know-it
</details>
# 答案1
**得分**: 1
问题在于您需要分配一个新的类来捕获`x`、`y`和`z`变量(然后分配一个新的委托实例来指向这个新类的方法)。
唯一避免这个问题的方法是避免捕获这些变量。一种方法是调整`Notify`的签名,以接受一个"state"参数,并将其传递给`action`:
```csharp
protected void Notify<TState>(Action<TServiceListener, TState> action, TState state)
{
...
action(obj: listener, state: state);
}
然后,您可以声明一个struct类型来捕获x
、y
和z
:
private record struct Accelerations(double X, double Y, double Z);
然后,您可以使用这个结构来显式捕获这些变量:
private void NotifyNewAccelerometerValues(double x, double y, double z)
{
var accelerations = new Accelerations(x, y, z);
this.Notify(action: static (listener, state) => listener.OnAccelerometersChanged(state.X, state.Y, state.Z), state: accelerations);
...
}
(请注意使用static
lambda以确保不会意外捕获任何内容)。
现在,(listener, state) => listener.OnAccelerometersChanged(state.X, state.Y, state.Z)
lambda不再从其周围的作用域中捕获任何内容(它访问的所有内容都作为参数传递)。因此,编译器不需要生成一个额外的类来捕获任何内容,这也意味着它可以创建委托实例一次并对其进行缓存。
或者,您可以使用ValueTuple而不是显式类型:
private void NotifyNewAccelerometerValues(double x, double y, double z)
{
this.Notify(action: static (listener, state) => listener.OnAccelerometersChanged(state.x, state.y, state.z), state: (x, y, z));
...
}
(运行时在例如 string.Create
和在 SynchronizationContext.Post
中也使用了这种模式)。
英文:
The problem you have is that you need to allocate a new class to capture the x
, y
and z
variables (and then allocate a new delegate instance to point to a method on this new class).
To only way to avoid this is to avoid capturing these variables. One way to do this is tweaking the signature of Notify
to accept a "state" parameter, which it passes to action
:
protected void Notify<TState>(Action<TServiceListener, TState> action, TState state)
{
...
action(obj: listener, state: state);
}
Then you can declare a struct type which captures x
, y
and z
:
private record struct Accelerations(double X, double Y, double Z);
And you can use this to explicitly capture these variables:
private void NotifyNewAccelerometerValues(double x, double y, double z)
{
var accelerations = new Accelerations(x, y, z);
this.Notify(action: static (listener, state) => listener.OnAccelerometersChanged(state.X, state.Y, state.Z), state: accelerations);
...
}
(Note the use of a static
lambda to ensure that we don't accidentally capture anything).
The (listener, state) => listener.OnAccelerometersChanged(state.X, state.Y, state.Z)
lambda now doesn't capture anything from its surrounding scope (everything it accesses is passed in as a parameter). Therefore the compiler doesn't need to generate an extra class to capture anything, which also means that it can create the delegate instance once and cache it.
Alternatively, you can use a ValueTuple rather than an explicit type:
private void NotifyNewAccelerometerValues(double x, double y, double z)
{
this.Notify(action: static (listener, state) => listener.OnAccelerometersChanged(state.x, state.y, state.z), state: (x, y, z));
...
}
(The runtime uses this pattern in e.g. string.Create
and to a lesser extent in SynchronizationContext.Post
)
答案2
得分: 0
这是一个lambda表达式,编译器将确定此lambda的类型为Action<IMyInterface>
,并将其转换为一个类:
public class MyHiddenClass{
double X; double Y; double Z;
public MyHiddenClass(double x, double y, double z) => (X, Y, Z) = (x, y, z);
public void Method(IMyInterface listener) => listener.OnAccelerometersChanged(X, Y, Z);
}
因此,lambda应该只编译一次。然而,由于lambda捕获变量,每次调用方法时都必须创建一个新对象。
通常情况下,这不应该是一个很大的问题,因为对象很小,应该是非常短暂的。如果您看到对象数量大幅增加,可能存在内存泄漏问题,需要进行调查。您的Notify
方法听起来像是一种事件注册方法,注册事件而不删除它们是内存泄漏的常见来源。
如果您绝对想避免分配,您可以尝试将x/y/z值存储为可重用的对象字段,并在此对象上使用方法,而不是lambda。我认为这可能可以避免分配,因为它避免了捕获变量,但我不确定。
英文:
> listener => listener.OnAccelerometersChanged(x, y, z)
This is a lambda, the compiler will figure out that the type of this lambda is Action<IMyInterface>
and essentially lower this into a class:
public class MyHiddenClass{
double X; double Y; double Z;
public MyHiddenClass(double x, double y, double z) => (X, Y, Z) = (x, y, z);
public void Method(IMyInterface listener) => listener.OnAccelerometersChanged(X, Y, Z);
}
So the lambda should only ever be compiled once. However, since the lambda is capturing variables it has to create a new object each time your method is called.
Normally this should not be a huge problem, since the object is small, and should presumably be very short lived. If you see a large increase in object count you may have a memory leak that you need to investigate. Your Notify
method sounds like some kind of event registration method, and registering events without removing them is a common source of memory leaks.
If you absolutely want to avoid allocations you could try storing your x/y/z values as fields in a object that is reused, and use a method on this object instead of a lambda. I believe that this may avoid allocations since it avoids capturing variables, but I'm not ceirtan.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论