单例,应用程序委托和顶层数据

本文深入探讨了Cocoa编程中全局变量的使用场景及其弊端,强调了单例模式在管理全局状态时的优越性和灵活性。文章详细解释了如何在Cocoa框架下实现单例,提供了避免过度依赖全局变量和顶层对象的方法,以维护程序结构的清晰和模块化。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

原文地址:http://cocoawithlove.com/2008/11/singletons-appdelegates-and-top-level.html

如果你的某个类需要实现单例模式,那么应该在哪里实现?你应该如何管理和控制它?不同的实现方式有不同的优缺点。

全局变量简介

它们令人害怕

全局变量对于老练的程序员来说是令人不愉快的东西。他们认为,如果程序中充斥着全局变量(本来应该是局部变量)是一种结构上的失败,程序将完全不受控制。

本文将全面介绍全局变量的声明和使用。

它们必不可少

事实上,程序中需要这些全局状态,全局变量必不可少。如果变量符合下列条件,则它应当是一个全局变量:

  1. 没有任何对象拥有该对象,需要管理它或者要对它负责;
  2. 在整个程序中,该对象只有一个;
  3. 它不是常量(比如字符串或者数字)。

如果这些全都符合,那么你应该使用全局变量。

如果你还不明白,那么于此相反的情况(不是全局变量)应该是:

  1. 接受对象管理的成员变量;
  2. 接受对象管理的集合的成员(们);
  3. 一个#define宏或者常量(常量属于编译器状态,而不是代码)

在Cocoa中,它们并不是真正的全局变量

事实上,我将提到的这些在Cocoa中所谓的全局变量并不是纯粹标准C中的全局变量,但在Cocoa中,我们完全可以用这些方法作为全局变量使用。

我将提到顶级成员对象(即application delegate的成员),以及单例对象。

并解释,为什么它们会被认为是全局对象:

  • 应用程序对象是最先构造的对象,而其它对象按照不同层级先后依次构造,因此顶级对象的作用域是整个应用程序(就像一个全局对象)。应用程序委托被认为是应用程序对象的衍生(对于你永远不重载application类时尤其如此)。
  • 一个单例对象只能被分配一次(并且不可被删除)——因此它是类的唯一的全局实例。单例对象其实是以全局变量的形式储存的,它们永远不会用Objective C的方式访问(它们是以类方法的方式访问的),至少在实现中提供一些抽象方法。

应用程序委托和应用程序控制器

Cocoa程序员应知道,MainMenu.xib文件中创建了一个应用程序委托对象:

对于iPhoneSDK,则在MainWindow.xib文件。

在applicationDidFinishLaunching方法中,可以初始化一个可全局访问的变量:

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification

{

myGlobalObject = [[MyGlobalObject alloc] init];

}

假设myGlobalObject有一个getter方法,通过这种方式你就可以访问这个对象:

[[NSApp delegate] myGlobalObject]

或者(iPhone中):

[[UIApplication sharedApplication].delegate myGlobalObject]

由于delegate返回的是id类型(而不是你真实的delegate的类型),你需要把myGlobalObject的声明为属性,并个且把delegate对象用括号括住并转换为你真正的应用程序委托类,比如:

((MyAppDelegate *)[UIApplication sharedApplication].delegate).myGlobalObject

当然,我看到有人在应用程序委托头文件种使用宏来定义委托对象:

#define UIAppDelegate \

((MyAppDelegate *)[UIApplication sharedApplication].delegate)

然后用:

UIAppDelegate.myGlobalObject

来访问顶层对象(记住import 该头文件).

使用应用程序委托的弊处

上面的做法是可行的,但在我的程序中,我从不在应用程序委托中做除了这些以外的事情:

  • NSApplication委托方法(从applicationDidFinishLaunching:方法直到application的finalize方法)
  • 不在windows菜单条中的菜单事件处理(例如“Preferences”菜单)

使用你的AppDelegate对象去管理你的全局对象是件糟糕的事情,因为你容易把太多的东西放到顶层对象中去,AppDelegate会因此庞大、结构紊乱。这有违设计模式,被称作Big Ball of Mud(一团乱泥)。

它有损程序的结构表现在两方面。首先是封装。AppDelegate只应该关注AppDelegate的内容(比如NSApplication对象和相关状态)。如果把其他对象相关的数据放到其中,就会强行干扰其他对象的自我控制。

其次是关注分离。保管、管理程序中非应用程序相关的变量不是AppDelegate的职责。一个设计良好的程序应该将类组织在完全分离、自包含的对象内,把任何对象都放到AppDelegate中违反了这一思维。

Cocoa的单例

要实现封装,应该把全局数据创建到类中作为单独的模块。通过单例达到这一目的。

Apple有一个最基本的单例实现方式: Creating a Singleton Instance.

我个人习惯于把这些单例方法放到宏中,你可以从这里下载到我的宏定义文件:

SynthesizeSingleton.h file

如果你在类实现中导入了SynthesizeSingleton.h头文件,则你可以使用语句:

SYNTHESIZE_SINGLETON_FOR_CLASS(MyClassName);

将该语句置于@implementation MyClassName声明后,这样你的类自动会变成单例。

还需要加入这一句到MyClassName的头文件中:

+ (MyClassName *)sharedMyClassName;

这样当其他源文件导入MyClassName.h后才能找到单例的访问方法。

一旦类成为单例,你可以使用下面的语句访问单例对象:

[MyClassName sharedMyClassName];

注意:单例对象不需要显式地alloc和init(在第一此访问时会自动调用alloc和init),但如果你想实行初始化动作时仍然要实现默认的init方法。

单例的好处

设计良好的单例是分离的、自管理的对象。在AppDelegate中的变量与delegate对象毫无类似,一个单例只关注它自身的角色和职责。在Xcode中打开Mac OSX10.5文档,搜索shared开头的单词,可以看到苹果使用了单例模式创建“manager”对象,允许你get、set和操作程序中只创建一次的对象。

而且,单例通过方法进行访问,在某些实现中有一些抽象的实现——你可以从真单例模式转换为基于独立线程的实现,而不用改动接口部分。

结论

除非必要,不要使用全局变量。程序中绝大多数数据都有明显的非顶级对象的“父对象”。单例及顶级对象只应当包含真正属于顶级对象的数据。

Cocoa单例在有时候是有用的和有弹性的。你可以让AppDelegate保有顶级数据,但尽可能将范围限制于MainMenu.xib构造的对象。



<think>根据用户需求,需要在C#中实现持续监控一个bool变量,当该变量值为false时,持续显示一个报警弹窗(不重复弹出,即只弹一次直到变量变为true再关闭,然后下次再变为false时再弹)。但注意,用户要求“持续显示一报警弹窗而不重复弹出”,意味着当变量为false时,弹窗一直显示(直到变量变为true才关闭),而不是在false期间不断弹出新窗口。 然而,通常弹窗(如MessageBox)会阻塞线程,所以我们需要非阻塞的方式。这里我们可以考虑使用一个自定义窗口,并在变量变化时控制其显示隐藏。 思路: 1. 定义一个全局的bool变量(比如`isSafe`),我们监控这个变量。 2. 使用一个后台线程(或使用定时器)来检查这个变量的值。 3. 当变量变为false时,显示一个报警窗口(如果还没有显示的话);当变量变为true时,关闭报警窗口。 4. 为了避免重复创建窗口,我们只创建一个窗口实,然后根据需要显示或隐藏。 但是,注意:用户要求“持续显示”直到变量变为true。所以,当变量为false时,窗口应该一直显示在最前面(比如模态窗口或者非模态窗口但置顶)。 然而,在UI程序中,我们通常需要在UI线程上操作控件。因此,我们可以使用一个System.Windows.Forms.Timer(如果使用WinForms)来检查变量,并在UI线程上操作窗口。 另外,我们可以使用事件机制来响应变量变化(而不是轮询),这样更高效。我们可以定义一个类来封装被监控的变量,当变量变化时触发事件。 根据引用[2]中提供的思路,我们可以定义一个类,其中包含一个事件,当值变化时触发事件。然后我们在事件处理函数中控制报警窗口的显示隐藏。 具体步骤: 步骤1:定义一个带有事件通知的类(如`MonitoredVariable`)来包装bool变量。 步骤2:在事件处理函数中,当新值为false时,显示报警窗口(如果窗口未显示);当新值为true时,关闭报警窗口。 步骤3:注意,我们只希望有一个报警窗口实,所以我们在类中保存一个对报警窗口的引用。 步骤4:报警窗口需要设计为当显示时,一直保持置顶,直到被关闭(这里我们不是真的关闭窗口,而是隐藏,以便下次使用)。 但是,用户要求“持续显示”报警弹窗,意味着在变量为false期间,窗口一直显示。因此,我们可以在变量变为false时显示窗口,在变为true时隐藏窗口。 考虑到UI线程,我们确保在UI线程上创建操作窗口。 下面我们实现这个方案: 首先,我们创建一个自定义的报警窗口(如`AlertWindow`),它只是一个简的窗体,上面显示报警信息。 然后,我们创建一个监控类: 注意:为了避免跨线程访问UI控件的问题,我们使用控件的Invoke方法(如果需要从非UI线程更新UI)。 但是,由于我们使用事件,并且事件可能由非UI线程触发,所以我们需要在事件处理中切换到UI线程。 这里我们假设整个程序是WinForms应用程序。 代码示: 1. 创建报警窗口(AlertWindow): 在Visual Studio中,添加一个Windows Form,命名为`AlertWindow`,然后设置一些报警文本。也可以简使用一个Label。 2. 创建监控类`MonitoredBool`: ```csharp public class MonitoredBool { private bool _value; private AlertWindow _alertWindow; // 报警窗口实 public event EventHandler ValueChanged; public bool Value { get => _value; set { if (_value != value) { _value = value; OnValueChanged(EventArgs.Empty); } } } protected virtual void OnValueChanged(EventArgs e) { ValueChanged?.Invoke(this, e); } // 显示报警窗口(在UI线程上) public void ShowAlertWindow(Form mainForm) { // 确保在UI线程上执行 if (mainForm.InvokeRequired) { mainForm.Invoke(new Action(() => ShowAlertWindow(mainForm))); return; } // 如果窗口不存在,则创建 if (_alertWindow == null || _alertWindow.IsDisposed) { _alertWindow = new AlertWindow(); // 设置报警窗口的父窗体为mainForm(可选),或者设置为无父窗体的弹出窗口 _alertWindow.StartPosition = FormStartPosition.CenterScreen; _alertWindow.TopMost = true; // 置顶 } // 显示窗口(非模态,因为我们需要在后台继续运行) if (!_alertWindow.Visible) { _alertWindow.Show(); } } // 隐藏报警窗口 public void HideAlertWindow(Form mainForm) { if (mainForm.InvokeRequired) { mainForm.Invoke(new Action(() => HideAlertWindow(mainForm))); return; } if (_alertWindow != null && !_alertWindow.IsDisposed && _alertWindow.Visible) { _alertWindow.Hide(); } } } ``` 3. 在主窗体中,我们初始化`MonitoredBool`,并订阅其`ValueChanged`事件。 ```csharp public partial class MainForm : Form { private MonitoredBool _monitoredBool = new MonitoredBool(); public MainForm() { InitializeComponent(); // 订阅事件 _monitoredBool.ValueChanged += MonitoredBool_ValueChanged; } private void MonitoredBool_ValueChanged(object sender, EventArgs e) { if (_monitoredBool.Value) // 如果值为true,则隐藏报警窗口 { _monitoredBool.HideAlertWindow(this); } else // 如果值为false,则显示报警窗口 { _monitoredBool.ShowAlertWindow(this); } } // 示:改变监控变量的值(如通过某个按钮点击事件) private void btnSetFalse_Click(object sender, EventArgs e) { _monitoredBool.Value = false; } private void btnSetTrue_Click(object sender, EventArgs e) { _monitoredBool.Value = true; } } ``` 4. 注意:在报警窗口(AlertWindow)中,我们可能还需要处理关闭事件。如果用户手动关闭了报警窗口,那么下次需要重新创建。但是,在我们的代码中,当需要显示时,如果窗口被关闭了(IsDisposed为true),我们会重新创建。 但是,这里有一个问题:如果用户手动关闭了报警窗口,那么当变量再次变为false时,会重新创建并显示。这符合要求吗? 用户要求是“持续显示”,所以手动关闭应该视为用户暂时关闭了报警,但变量还是false,所以应该继续弹出?或者我们不允许用户关闭?我们可以设置报警窗口没有关闭按钮(ControlBox=false),这样用户只能通过程序逻辑来关闭。 因此,我们可以修改Alert窗口,去掉关闭按钮,只允许通过监控变量为true来关闭(隐藏)。这样,当变量为false时,窗口将一直显示,用户无法关闭。 修改AlertWindow的构造函数: ```csharp public AlertWindow() { InitializeComponent(); this.ControlBox = false; // 去掉关闭按钮 this.FormBorderStyle = FormBorderStyle.FixedDialog; // 固定边框 } ``` 这样,用户就不能通过点击关闭按钮来关闭窗口了,只能通过程序隐藏。 5. 另外,我们可能希望报警窗口总是置顶,所以设置`TopMost = true`。 6. 但是,上述代码中,每次变量变化都会触发事件,然后显示或隐藏窗口。这样,当变量为false时,只显示一次窗口(如果已经显示则不再重复显示),当变量变为true时隐藏窗口。 7. 注意:在多线程环境中,如果改变监控变量的值是在非UI线程,那么我们需要确保对`MonitoredBool.Value`的赋值是线程安全的,或者使用Invoke。但是,在`MonitoredBool`类中,我们的事件处理已经通过主窗体的Invoke确保了UI操作在UI线程上执行。 8. 另外,我们也可以考虑使用`SynchronizationContext`来捕获UI线程上下文,这样就不需要传递主窗体实了。但为了简,我们传递了主窗体。 优化:我们可以修改`MonitoredBool`类,使其不依赖主窗体实,而是使用`SynchronizationContext`来发送事件到UI线程。 但为了简,我们使用上述方法。 9. 另一种方法:使用BackgroundWorker或Timer轮询,但事件驱动更高效。 10. 关于持续监控:我们已经通过事件实现了即时响应。 总结:这个方案满足了需求,当变量变为false时,显示一个置顶的、无关闭按钮的报警窗口,持续显示直到变量变为true才隐藏。 但是,用户要求的是“报警弹窗”,我们这里使用了自定义窗口,而不是MessageBox(因为MessageBox会阻塞线程)。所以,我们使用非阻塞的自定义窗口。 另外,用户要求“不重复弹出”,我们的方案中,当变量为false时,只显示一次窗口(不会重复弹出多个窗口)。 注意:如果监控的变量在falsetrue之间快速切换,可能会导致窗口频繁显示隐藏。但由于事件是即时触发的,所以窗口会随之变化。 如果用户需要的是在变量为false期间,即使快速切换,窗口也只弹出一次(直到下一次从true变为false再弹出),那么我们的逻辑已经满足:因为每次从true变为false都会触发显示,false变为true触发隐藏。所以,如果变量在短时间内多次变化,窗口也会频繁显示隐藏。为了避免闪烁,我们可以加入一个延迟(如,当变量变为false后,显示窗口并持续至少几秒钟),但用户没有这个要求,所以不添加。 如果用户希望避免过于频繁的切换,可以加入防抖(debounce)或节流(throttle)机制,但根据需求,我们不需要。 因此,上述代码应该可以满足用户的需求。 注意:在实际应用中,监控的变量可能由其他线程改变,所以需要确保对`MonitoredBool.Value`的赋值是线程安全的(如使用lock)。但这里我们假设对Value的赋值是在同一个线程(比如UI线程)或者即使多线程赋值,但每次赋值都是原子的(bool是原子操作)并且我们只关心变化,所以暂时不加锁。如果多线程赋值,可能会多次触发事件,但事件处理中通过Invoke到UI线程,所以不会冲突。 下面我们给出完整的主窗体代码监控类代码,以及报警窗口的简设计。 报警窗口(AlertWindow)设计: - 添加一个Label,Text为“报警!发生异常!” - 设置窗体属性:ControlBox=false, FormBorderStyle=FixedDialog, TopMost=true 主窗体(MainForm)设计: - 添加两个按钮:btnSetFalsebtnSetTrue,分别用于将监控变量设为falsetrue。 监控类(MonitoredBool)如上。 这样,当点击btnSetFalse时,报警窗口出现,并且无法关闭(只能通过程序隐藏)。当点击btnSetTrue时,报警窗口隐藏。 因此,我们实现了需求。 但是,用户可能希望监控的变量来自其他位置(比如硬件状态等),那么只需要在变量变化时更新`MonitoredBool.Value`即可。 注意:如果变量是在后台线程更新的,那么赋值给`MonitoredBool.Value`时,需要确保线程安全。我们可以使用lock来保护_value的读写。 修改`MonitoredBool`类,增加锁: ```csharp private object _lock = new object(); public bool Value { get { lock (_lock) { return _value; } } set { lock (_lock) { if (_value != value) { _value = value; OnValueChanged(EventArgs.Empty); } } } } ``` 这样,多线程赋值就是安全的。 最后,根据引用[3],用户提到了通过窗口标题关闭窗口,但这里我们不需要,因为我们自己控制窗口的显示隐藏。 因此,以上代码即为解决方案。 相关问题: 1. 如何在C#中实现跨线程更新UI? 2. 如何创建一个始终置顶的窗口? 3. 在C#中,如何实现一个带有事件通知的属性? 4. 如何防止用户关闭一个Windows窗体?</think>### C# 持续监控 bool 变量并显示一报警弹窗的实现方案 在 C# 中实现持续监控 bool 变量,并在值为 `false` 时显示一报警弹窗(不重复弹出),可以通过以下步骤实现: #### 核心思路 1. 使用 `System.ComponentModel.INotifyPropertyChanged` 接口实现属性变更通知 2. 通过事件触发机制响应值变化 3. 使用标志位控制弹窗状态 4. 确保弹窗只显示一次直到状态恢复 #### 完整代码实现 ```csharp using System; using System.ComponentModel; using System.Threading; using System.Windows.Forms; // 实现属性变更通知的监控类 public class BoolMonitor : INotifyPropertyChanged { private bool _isSafe; private bool _isAlertShowing = false; // 标记弹窗状态 public bool IsSafe { get => _isSafe; set { if (_isSafe != value) { _isSafe = value; OnPropertyChanged(nameof(IsSafe)); } } } public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged(string propertyName) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); // 当值变为 false 且弹窗未显示时触发报警 if (propertyName == nameof(IsSafe) && !IsSafe && !_isAlertShowing) { _isAlertShowing = true; ShowAlert(); } // 当值恢复为 true 时重置弹窗状态 else if (propertyName == nameof(IsSafe) && IsSafe) { _isAlertShowing = false; } } private void ShowAlert() { // 在 UI 线程上显示弹窗 Thread alertThread = new Thread(() => { using (AlertForm alert = new AlertForm()) { // 持续监控状态,直到值恢复为 true while (!IsSafe) { if (!alert.Visible) alert.ShowDialog(); Application.DoEvents(); // 处理窗口消息 Thread.Sleep(100); // 降低 CPU 占用 } alert.Close(); } _isAlertShowing = false; }); alertThread.SetApartmentState(ApartmentState.STA); alertThread.Start(); } } // 报警弹窗窗体 public class AlertForm : Form { public AlertForm() { this.Text = "安全警报"; this.Size = new System.Drawing.Size(300, 200); Label lblMessage = new Label { Text = "⚠️ 检测到危险状态!请立即处理!", Dock = DockStyle.Fill, TextAlign = System.Drawing.ContentAlignment.MiddleCenter, Font = new System.Drawing.Font("微软雅黑", 12) }; this.Controls.Add(lblMessage); } } // 主程序使用示 public partial class MainForm : Form { private readonly BoolMonitor _safetyMonitor = new BoolMonitor(); public MainForm() { InitializeComponent(); _safetyMonitor.PropertyChanged += SafetyMonitor_PropertyChanged; } private void SafetyMonitor_PropertyChanged(object sender, PropertyChangedEventArgs e) { // 可在此添加其他响应逻辑 } // 示:修改监控变量(实际应用中可能来自传感器或业务逻辑) private void btnSimulateDanger_Click(object sender, EventArgs e) { _safetyMonitor.IsSafe = false; // 触发报警 } private void btnResolveDanger_Click(object sender, EventArgs e) { _safetyMonitor.IsSafe = true; // 解除报警 } } ``` #### 关键机制说明 1. **一弹窗控制** - 通过 `_isAlertShowing` 标志位确保弹窗只创建一次 - 当值从 `false` 变为 `true` 时重置标志位:`_isAlertShowing = false` 2. **持续监控实现** - 在 `ShowAlert()` 中使用 `while (!IsSafe)` 循环持续检查状态 - 弹窗保持显示直到 `IsSafe` 变为 `true` 3. **线程安全处理** - 使用独立线程显示弹窗:`alertThread.SetApartmentState(ApartmentState.STA)` - 通过 `Application.DoEvents()` 处理窗口消息队列 4. **资源优化** - `Thread.Sleep(100)` 降低 CPU 占用率 - `using` 语句确保弹窗资源正确释放 #### 引用说明 该方案参考了属性变更通知模式[^2]线程安全弹窗的实现原理[^3],通过事件驱动机制避免轮询带来的性能损耗,同时确保 UI 操作的线程安全性。 --- ### 相关问题 1. **如何在 C# 中实现后台线程持续监控变量变化?** > 可通过 `BackgroundWorker` 或 `System.Threading.Timer` 实现后台监控,需注意线程同步问题[^2]。 2. **如何使报警弹窗始终显示在最顶层?** > 设置窗体属性 `TopMost = true`,并重写 `OnActivated` 方法保持焦点: > ```csharp > protected override void OnActivated(EventArgs e) > { > base.OnActivated(e); > this.BringToFront(); > } > ``` 3. **如何避免重复弹窗时产生多个窗口实?** > 使用模式管理弹窗实: > ```csharp > private static AlertForm _instance; > public static AlertForm Instance > { > get => _instance ??= new AlertForm(); > } > ``` 4. **如何在关闭弹窗后自动恢复监控?** > 在窗体 `FormClosing` 事件中重置监控状态: > ```csharp > void AlertForm_FormClosing(object sender, FormClosingEventArgs e) > { > if (e.CloseReason == CloseReason.UserClosing) > _monitor.ResetAlertState(); > } > ``` [^1]: 关于布尔值在不同语言中的内存差异封送处理方案 [^2]: 使用 INotifyPropertyChanged 实现属性变更通知的委托模式 [^3]: 通过窗口句柄操作实现进程管理的技术方案
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值