建议94:区别对待override和new

本文通过示例代码详细解释了C#中override和new关键字的区别,包括它们如何影响继承体系中的方法调用和多态行为。

建议94:区别对待override和new

override和new使类型体系应为继承而呈现出多态性。多态要求子类具有与基类同名的方法,override和new的作用就是:

  • 如果子类中的方法前面带有new关键字,则该法被定义为独立于基类的方法。
  • 如果子类中的方法前面带有override关键字,则子类的对象将调用该方法。而不调用基类的方法。

我们来看一个继承体系:

复制代码
        public class Shape
        {
            public virtual void MethodVirtual()
            {
                Console.WriteLine("base MethodVirtual call");
            }

            public void Method()
            {
                Console.WriteLine("base Method call");
            }
        }

        class Circle : Shape
        {
            public override void MethodVirtual()
            {
                Console.WriteLine("circle override MethodVirtual");
            }
        }

        class Rectangle : Shape
        {

        }

        class Triangle : Shape
        {
            public new void MethodVirtual()
            {
                Console.WriteLine("triangle new MethodVirtual");
            }

            public new void Method()
            {
                Console.WriteLine("triangle new Method");
            }
        }

        class Diamond : Shape
        {
            public void MethodVirtual()
            {
                Console.WriteLine("Diamond default MethodVirtual");
            }

            public void Method()
            {
                Console.WriteLine("Diamond default Method");
            }
        }
复制代码

Shape是所有子类的基类。

Circle类override父类的MethodVirtual,所以即使子类转型为Shape,调用的还是子类方法:

            Shape s = new Circle();
            s.MethodVirtual();
            s.Method();

输出为:

circle override MethodVirtual
base Method call

            Circle s = new Circle();
            s.MethodVirtual();
            s.Method();

输出也为:

circle override MethodVirtual
base Method call

类型Rectangle没有对基类做任何处理,所以无论是否转型为Shape,调用的都是基类Shape的方法。
类型Triangle将基类Shape的virtual方法和非virtual方法都new了一般,所以第一种方法为:

            Shape s = new Triangle();
            s.MethodVirtual();
            s.Method();

因为子类应经new了父类的方法,故子类方法和基类方法完全没有关系了,只要s被转型为Shape,针对s调用搞得都是父类方法。

            Triangle triangle = new Triangle();
            triangle.MethodVirtual();
            triangle.Method();

调用的都是子类方法,输出为:

triangle new MethodVirtual
triangle new Method


类型Diamond包含了两个和基类一模一样的方法,并且没有额外的修饰符。这在编译器中会提出警示。但是如果选择忽略这些警示,程序还是一样可以运行。

            Shape s=new Diamond();
            s.MethodVirtual();
            s.Method();

编译器会默认new的效果,所以输出和显示设置为new时一样。

输出为:

base MethodVirtual call
base Method call

            Diamond s = new Diamond();
            s.MethodVirtual();
            s.Method();

输出为:

Diamond default MethodVirtual
Diamond default Method
最后给一个综合示例:

复制代码
 static void Main(string[] args)
        {
            TestShape();
            TestDerive();
            TestDerive2();
        }

        private static void TestShape()
        {
            Console.WriteLine("TestShape\tStart");
            List<Shape> shapes = new List<Shape>();
            shapes.Add(new Circle());
            shapes.Add(new Rectangle());
            shapes.Add(new Triangle());
            shapes.Add(new Diamond());
            foreach (Shape s in shapes)
            {
                s.MethodVirtual();
                s.Method();
            }
            Console.WriteLine("TestShape\tEnd\n");
        }

        private static void TestDerive()
        {
            Console.WriteLine("TestDerive\tStart");
            Circle circle = new Circle();
            Rectangle rectangle = new Rectangle();
            Triangle triangel = new Triangle();
            Diamond diamond = new Diamond();
            circle.MethodVirtual();
            circle.Method();
            rectangle.MethodVirtual();
            rectangle.Method();
            triangel.MethodVirtual();
            triangel.Method();
            diamond.MethodVirtual();
            diamond.Method();
            Console.WriteLine("TestShape\tEnd\n");
        }

        private static void TestDerive2()
        {
            Console.WriteLine("TestDerive2\tStart");
            Circle circle = new Circle();
            PrintShape(circle);
            Rectangle rectangle = new Rectangle();
            PrintShape(rectangle);
            Triangle triangel = new Triangle();
            PrintShape(triangel);
            Diamond diamond = new Diamond();
            PrintShape(diamond);
            Console.WriteLine("TestDerive2\tEnd\n");
        }

        static void PrintShape(Shape sharpe)
        {
            sharpe.MethodVirtual();
            sharpe.Method();
        }
复制代码

输出为:

TestShape       Start
circle override MethodVirtual
base Method call
base MethodVirtual call
base Method call
base MethodVirtual call
base Method call
base MethodVirtual call
base Method call
TestShape       End

TestDerive      Start
circle override MethodVirtual
base Method call
base MethodVirtual call
base Method call
triangle new MethodVirtual
triangle new Method
Diamond default MethodVirtual
Diamond default Method
TestShape       End

TestDerive2     Start
circle override MethodVirtual
base Method call
base MethodVirtual call
base Method call
base MethodVirtual call
base Method call
base MethodVirtual call
base Method call
TestDerive2     End



 

转自:《编写高质量代码改善C#程序的157个建议》陆敏技

在 Apache Flink 中,`ProcessFunction` 的两个关键上下文对象是: - `Context` - `ReadOnlyContext` 它们出现在 `KeyedProcessFunction<K, I, O>.processElement(I value, Context ctx, Collector<O> out)` 方法中。 --- ## ✅ 核心区别总结 | 特性 | `Context` | `ReadOnlyContext` | |------|-----------|-------------------| | 是否可写状态 | ✅ 可以访问所有状态(包括修改) | ❌ **只能读取**状态,不能修改 | | 是否可注册定时器 | ✅ 可以注册/删除定时器 | ❌ **不能注册或删除定时器** | | 使用场景 | 通用处理逻辑 | 仅用于查询状态(如广播流中的只读访问) | | 出现场景 | 主输入流的 `processElement` | 广播状态中副输入流的 `processBroadcastElement` | --- ## ✅ 详细解释 ### 📍 1. `Context`(全功能上下文) 这是主输入流中使用的完整上下文,允许你: - 访问 **Keyed State** - 注册 **Processing Time / Event Time 定时器** - 获取当前元素的元数据(timestamp、element topic、partition 等) - 写入或更新状态 - 删除定时器 ```java public void processElement( Tuple2<String, Integer> value, Context ctx, Collector<String> out) throws Exception { // ✅ 获取当前 key 的状态 ValueState<Integer> state = getRuntimeContext().getState( new ValueStateDescriptor<>("counter", Integer.class) ); // ✅ 修改状态 state.update(state.value() == null ? 1 : state.value() + 1); // ✅ 注册一个 10 秒后的定时器 long timerTs = ctx.timerService().currentProcessingTime() + 10_000; ctx.timerService().registerProcessingTimeTimer(timerTs); // ✅ 获取事件时间戳 long timestamp = ctx.timestamp(); } ``` > ⚠️ `Context` 是 `ProcessFunction` 主流处理的标准上下文。 --- ### 📍 2. `ReadOnlyContext`(只读上下文) 它主要用于 **广播状态模式(Broadcast State Pattern)** 中的 **非广播流(broadcast stream)一侧**。 当你使用: ```java broadcastStream.connect(nonBroadcastStream).process(new BroadcastProcessFunction<>()) ``` 那么: - `processBroadcastElement(...)` → 使用 `ReadOnlyContext` - `processElement(...)` → 使用 `Context` #### 🔹 特点: - ❌ **不能注册定时器** - ❌ **不能修改任何 keyed state** - ✅ 只能读取 keyed state - ✅ 可以读取广播状态(Broadcast State) ```java public class MyBroadcastProcessFunction extends BroadcastProcessFunction<AlertRule, DataEvent, String> { // 处理广播流:规则更新 @Override public void processBroadcastElement( AlertRule rule, ReadOnlyContext ctx, Collector<String> out) throws Exception { // ✅ 可以获取广播状态并写入(因为是广播流本身) BroadcastState<String, AlertRule> broadcastState = ctx.getBroadcastState( new MapStateDescriptor<>("rules", String.class, AlertRule.class) ); broadcastState.put(rule.getId(), rule); // ✅ 允许写入广播状态 // ❌ 不能注册定时器 // ctx.timerService().registerProcessingTimeTimer(...); // 编译报错! // ❌ 不能访问普通 keyed state(也不能改) // ValueState x = getRuntimeContext().getState(...); // 虽然语法上可能通过,但不推荐 } // 处理主流:数据事件 @Override public void processElement( DataEvent event, ReadOnlyContext ctx, // 注意!这里是 ReadOnlyContext Collector<String> out) throws Exception { // ✅ 可以读取广播状态(规则) BroadcastState<String, AlertRule> broadcastState = ctx.getBroadcastState( new MapStateDescriptor<>("rules", String.class, AlertRule.class) ); for (Map.Entry<String, AlertRule> entry : broadcastState.immutableEntries()) { AlertRule rule = entry.getValue(); if (rule.matches(event)) { out.collect("Alert triggered by rule: " + rule.getName()); } } // ❌ 不能注册定时器 // ctx.timerService().registerProcessingTimeTimer(...); // 不允许! // ✅ 可以读取当前 key 的状态(只读视图) ValueState<Long> countState = getRuntimeContext().getState( new ValueStateDescriptor<>("count", Long.class) ); Long current = countState.value(); // ✅ 只读没问题 // countState.update(current + 1); // ❌ 虽然语法可行,但在某些版本中受限或不保证行为 } } ``` > ⚠️ 关键点:`ReadOnlyContext` 中虽然可以通过 `getRuntimeContext()` 获取状态,但**Flink 不鼓励你在 `processElement` 使用 `ReadOnlyContext` 时修改 keyed state**。你应该把它当作“只读”来对待。 --- ## ✅ 为什么设计 `ReadOnlyContext`? Flink 设计 `ReadOnlyContext` 的主要原因是: ### 🔐 安全性语义清晰 - 在广播连接(`connect(broadcastStream, nonBroadcastStream)`)中: - 广播流用于分发配置/规则 - 非广播流用于处理数据 - 如果允许非广播流随意注册定时器或修改状态,会导致: - 每个元素都可能触发定时器 → 性能爆炸 - 状态混乱,难以追踪 - 所以 Flink 明确限制为 **只读上下文** --- ## ✅ 如何绕过限制?(如果你真的需要定时器) 如果你想在接收到广播消息后为主流设置一些行为(比如动态开启监控),你可以: ### ✔️ 方案:在 `processElement` 中根据广播状态决定是否注册定时器 ```java // 在 processElement 中使用 Context(不是 ReadOnlyContext) // 并结合广播状态判断是否启用延迟处理 if (broadcastState.get("enable-delay") != null) { ctx.timerService().registerProcessingTimeTimer( ctx.timerService().currentProcessingTime() + 60_000 ); } ``` 即:**真正的定时器注册状态修改,仍然发生在主流的 `processElement` 使用 `Context` 的地方**,而不是在广播侧。 --- ## ✅ 总结对比表 | 功能 | `Context` | `ReadOnlyContext` | |------|----------|-------------------| | 所属函数 | `processElement`(主流) | `processBroadcastElement` `processElement`(广播连接时) | | 是否可读 keyed state | ✅ | ✅(只读) | | 是否可写 keyed state | ✅ | ❌(不推荐,即使语法允许) | | 是否可注册定时器 | ✅ | ❌ | | 是否可访问广播状态 | ✅ | ✅ | | 是否可修改广播状态 | ✅(仅在广播流 side) | ✅(仅在广播流 side) | | 典型用途 | 正常事件处理、状态更新、定时任务 | 读取广播配置、规则匹配 | --- ## ✅ 最佳实践建议 1. **不要尝试在 `ReadOnlyContext` 中注册定时器或修改普通状态** 2. **将广播状态视为“配置分发通道”** 3. **真正的状态变更定时器操作留在主流的 `processElement` 中完成** 4. **确保广播状态的设计是幂等可合并的** ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值