资讯动态

高级VB编程进阶:类、接口、异常处理与反射插件实战

发布时间:2026/10/14 14:40:29 来源:尧图企业网站定制
简介面向已掌握VB基础语法、希望进阶的中高级开发者这份资料以Matthew Curland《Advanced Visual Basic》知识体系为主线覆盖OOP设计、事件处理、多线程、数据库访问、COM组件交互等高级主题既能帮助读者厘清“高级VB到底学什么”也提供大量可直接参考和复用的工程源码。压缩包为rar格式共327个文件、10.33MB类型以bas模块、cls类模块、frm窗体、vbp工程文件及ctl用户控件为主辅以odl、ocx、dll等COM相关文件另有pdf电子文档兼顾理论讲解与实战代码。其中VBoost.bas、ROTHook.bas、CDECLFunctionDelegator.bas、ThreadOlePump.bas、StackAllocZero.bas等底层模块颇具价值涉及内存分配、函数委托、线程消息泵等进阶技巧读者可对照源码理解高级VB的实现精髓并将相关思路直接移植到自己的项目中加速开发。目前已有355人学习下载。1. 高级VB编程到底难在哪从「能跑」到「扛得住」高级VB编程Advanced Visual Basic 这个说法圈子里经常被误读成「多记几个冷门函数」。我在接手某公司的进销存系统改造时见过一个典型案例同一个日志写入逻辑在三个窗体里复制了七份改一次格式要改七处一个查询条件用字符串拼接了两百行数据库一超时就整个界面白屏。真正的高级不是函数记得多而是类与接口、结构化异常处理、Win32 API 互操作、数据访问封装这四块地基打得稳。这篇笔记不聊语法手册直接把这四条主线拆开每段配可复现的代码适合正在用 VB 写业务系统、又总觉得代码一复杂就改不动的人。2. 用类模块和接口重构 VB 代码从过程式思维切换到对象式思维高级 VB 编程和入门 VB 最大的分水岭不是语法而是代码组织方式。入门阶段一个窗体塞几十个事件函数跑得通就算成功等业务规则复杂到前后互相牵制就发现函数级复用救不了你。类模块Class才是承载状态的容器也是后面所有高级特性的地基。2.1 为什么高级 VB 的第一课是类而不是函数函数解决的是「重复代码」类解决的是「分散的状态」。举个例子某进销存系统早期版本里三个窗体都要写日志。A同学的做法是每个窗体各自写一套文件写入代码路径直接写死在窗体里。等部署到新机器发现日志目录不存在三处都要改一遍。这个时候把日志逻辑抽成函数已经不够——因为每处还各自记录了自己的格式、编码、是否追加抽函数要带一堆参数调用方依然乱成一团。类的思路是把这些参数变成属性把日志写入变成方法把「有日志发生」变成事件通知。调用方只需要 New 一次日志类之后所有行为由这个对象自己负责状态不再散落在各个窗体里。用 VB 写类这件事VB6 时代就有类模块但继承能力残缺接口用起来也晦涩VB.NET 才把类、接口、继承补完整。我一般建议业务系统至少从「工具类」和「实体类」两类开始工具类封装 IO、加解密、格式转换实体类封装一条业务数据及其校验规则。这两类写顺手了再谈接口多态。判断标准就一条这段逻辑里有没有「状态」有就放进类没有才考虑函数。2.2 一个可复用的日志类属性、方法、事件一把抓下面是一个文件日志类的完整实现。它同时展示了属性、方法、事件、资源释放这几件事是很好的进阶范本Imports System.IO Imports System.Text Public Class FileLogger Private _logPath As String Private _append As Boolean True 对外暴露日志文件路径允许运行期调整 Public Property LogPath() As String Get Return _logPath End Get Set(ByVal value As String) _logPath value End Set End Property 日志事件UI 层可以订阅它做状态栏提示 Public Event MessageLogged(ByVal level As String, ByVal message As String) Public Sub New(ByVal path As String) _logPath path End Sub Public Sub Log(ByVal level As String, ByVal message As String) 先保证目录存在避免首次写入抛异常 Dim dir As String Path.GetDirectoryName(_logPath) If Not String.IsNullOrEmpty(dir) AndAlso Not Directory.Exists(dir) Then Directory.CreateDirectory(dir) End If Dim line As String String.Format([{0}] [{1}] {2}, Now.ToString(yyyy-MM-dd HH:mm:ss), level, message) Using 确保 StreamWriter 在异常时也能关闭 Using writer As New StreamWriter(_logPath, _append, Encoding.UTF8) writer.WriteLine(line) End Using RaiseEvent MessageLogged(level, message) End Sub End Class调用端更简单Dim logger As New FileLogger(D:\logs\app.log) AddHandler logger.MessageLogged, Sub(l As String, m As String) Console.WriteLine(日志已写入 l - m) logger.Log(INFO, 系统启动)代码里几个值得留意的点。一是 Log 方法内先创建目录这是血泪经验——很多程序第一次跑在干净机器上就挂在日志写入目录不存在是头号原因。二是 Using 语句StreamWriter 实现了 IDisposable用 Using 包起来无论正常写完还是中途异常文件句柄都会被释放很多人手动写 Close一旦前面抛异常Close 根本执行不到。三是 _append 参数设为 True 表示追加写日志文件不会被覆盖。事件的用法也说明一下日志类不关心谁在听只管把事件抛出去。UI 层用 AddHandler 订阅就能在界面上看到实时日志而日志类本身不依赖任何窗体。这是解耦最常见的落点也是从「调用别人」到「被别人通知」的思维转变。2.3 接口与多态让新增日志类型不用改调用方类解决了状态接口解决的是「换实现不换调用」。假设同一个系统里调试时要输出到控制台生产要多文件记录审计要求写数据库。如果业务代码各自调用具体类未来每加一种记录方式所有调用点都要跟着改。先定义一个接口约定「能做这件事」Public Interface ILogger ReadOnly Property Name() As String Sub WriteLog(ByVal level As String, ByVal message As String) End Interface然后让 FileLogger 和 ConsoleLogger 分别实现这个接口Public Class FileLogger Implements ILogger Public ReadOnly Property Name() As String Implements ILogger.Name Get Return FileLogger End Get End Property Public Sub WriteLog(ByVal level As String, ByVal message As String) Implements ILogger.WriteLog 写入文本文件实现同上一节此处省略重复代码 End Sub End Class Public Class ConsoleLogger Implements ILogger Public ReadOnly Property Name() As String Implements ILogger.Name Get Return ConsoleLogger End Get End Property Public Sub WriteLog(ByVal level As String, ByVal message As String) Implements ILogger.WriteLog Console.WriteLine([{0}] {1}, level, message) End Sub End Class业务代码只需要面向接口声明Dim logs As New List(Of ILogger) logs.Add(New FileLogger()) logs.Add(New ConsoleLogger()) For Each lg As ILogger In logs lg.WriteLog(INFO, 订单已保存) Next新增一个数据库日志类时只要实现 ILogger上面这段 For 循环一行都不用改。这就是接口多态的实用价值。VB6 里 Implements 写法与此类似但 VB.NET 多了泛型 List(Of T)遍历更安全。接口相当于契约把「做什么」和「怎么做」分开这一条想通代码质量能上一个台阶后面讲反射插件框架时还会用到同一个套路。3. Win32 API 与 COM 互操作突破 VB 边界的两根支柱VB 的类库再丰富总有覆盖不到的系统底层能力。这时候要请出 Win32 API 和 COM。这两块是高级VB编程里最容易被神化、也最容易翻车的部分需要讲清楚边界。3.1 什么时候值得碰 Win32 API三个典型场景不是所有问题都要上 API。VB.NET 的 System.Environment、My.Computer 已经封装了大量系统信息能用封装就用封装。API 调用是黑匣子参数错一个就崩溃调试成本比普通代码高一个量级。值得碰的场景通常是这三类获取系统真实底层信息。比如临时目录、系统目录、屏幕物理分辨率某些精简版系统上 .NET 封装返回的值不一定可靠调用 GetTempPath、GetSystemMetrics 反而更准。控制窗口和进程行为。置顶窗口、鼠标穿透、任务栏隐藏、开机自启注册这些往往要动窗口样式或注册表标准控件没提供全部能力。和老系统做数据交换。某些行业老组件只暴露 C 接口VB 侧没有现成封装只能请 API 出面。判断标准只有一个类库做不到或者用类库做出来的代码比 API 更绕这时候才上。为了炫技去调 API后面维护的人会骂人。3.2 安全调用 API 的完整模板StringBuilder 缓冲与句柄位宽直接贴一个安全调用系统 API 的模板取系统临时目录Imports System.Runtime.InteropServices Imports System.Text Public Class NativeMethods CharSet.Unicode 表示按 UTF-16 传递字符串SetLastError 用于取错误码 DllImport(kernel32.dll, CharSet:CharSet.Unicode, SetLastError:True) Public Shared Function GetTempPath( ByVal nBufferLength As Integer, ByVal lpBuffer As StringBuilder) As Integer End Function End Class调用Dim buffer As New StringBuilder(260) Dim result As Integer NativeMethods.GetTempPath(buffer.Capacity, buffer) If result 0 AndAlso result buffer.Capacity Then Dim tempDir As String buffer.ToString() MessageBox.Show(tempDir) Else Dim errCode As Integer Marshal.GetLastWin32Error() MessageBox.Show(调用失败错误码 errCode.ToString()) End If这里有两个关键参数直接决定是否翻车。第一输出字符串必须用 StringBuilder不能直接用 String。String 是不可变类型API 往缓冲区写内容时String 内部没有预留可写空间轻则返回空串重则内存访问违规。StringBuilder 先按 260 长度预分配API 往里写再用 ToString 取回来。第二GetTempPath 的返回值是实际写入的字符数用它判断是否成功、是否截断比盲目信任函数本身靠谱。声明里的 CharSet.Unicode 不能少否则 ANSI 和 Unicode 版本错位中文路径直接乱码。句柄类型是另一个高频坑给个对照表C 原始类型常见错误声明VB.NET 建议HANDLELong / IntegerIntPtrDWORDLongUInteger计数类BOOLBooleanBooleanLPTSTR 输出缓冲StringStringBuilderHANDLE 在 64 位系统上是 64 位指针VB6 时代用 Long 是因为当年系统是 32 位现在直接照搬旧代码声明 Integer 或 Long高位被截断API 调用大概率立刻崩溃。这也是很多老项目迁移到 64 位时集中翻车的原因抄网上旧代码之前先核对这一列。3.3 COM 互操作把老组件请进来把新类送出去COM 是 Windows 上老牌组件协议VB6 写的组件天然就是 COM 组件。现在的业务系统里COM 互操作主要出现在两个方向。方向一VB.NET 调用旧的 COM 组件。老系统里已经稳定的组件没必要重写VB.NET 添加组件引用后可以直接 New也可以用 CreateObject 做后期绑定。后期绑定的好处是不依赖注册版本坏处是没有智能提示、运行期才暴露错误我一般只在调用方是动态脚本时才用。方向二把 VB.NET 写的类暴露成 COM给老 VB6 程序或其他脚本调用。常见做法是给类打上 ComVisible 和 Guid 特性然后编译注册Imports System.Runtime.InteropServices ComVisible(True) Guid(3F2504E0-4F89-41D3-9A0C-0305E82C3301) Public Class CustomerHelper Public Function GetCustomerName(ByVal customerId As Integer) As String 这里实现真实业务查询注意 COM 接口尽量只暴露简单类型 Return 客户- customerId.ToString() End Function End Class编译后用 SDK 自带的 regasm 工具注册加上 /codebase 参数表示允许从非全局程序集缓存位置加载。这里最容易被忽略的是位数匹配32 位注册工具注册的组件64 位进程调不到反之亦然。老系统如果是 32 位新模块就要用 32 位模式运行两边必须一致。COM 互操作调试起来比普通代码难十倍先把位数、注册状态这两个前提查清楚能省掉大半折腾时间。4. VB 的结构化异常处理与数据访问把稳定性写进代码结构业务系统里线上事故的根源一半是数据写错了一半是异常没接住。前者靠事务和参数化查询后者靠结构化异常处理。这一章把两件事一起解决。4.1 先别急着写业务用 Try/Catch/Finally 框住不稳定操作VB6 时代写异常处理是 On Error Resume Next 和 On Error GoTo前者吞掉错误继续跑后者让代码跳来跳去。On Error Resume Next 是很多老系统「看起来正常、一深用就崩」的根源——错误被静默吞掉等数据写坏了才发现。VB.NET 提供了结构化异常处理Try 块包住可能出错的操作Catch 捕获Finally 无论成败都执行清理Dim conn As New SqlConnection(_connectionString) Try conn.Open() 业务操作 Catch ex As Exception 至少写一条日志不要空 Catch Console.WriteLine(ex.ToString()) Throw 无法恢复就原样抛出让上层决定 Finally conn.Close() End Try三条铁律照着做能少踩一半坑。第一不要用空 Catch 吞异常至少写一条日志。日志都没有线上问题全靠猜猜错的代价是加倍的。第二需要清理资源时用 Finally或者直接用 Using。第三捕获后如果无法恢复用 Throw 原样抛出去不要改成返回 False 假装成功——假装成功比崩溃可怕得多崩溃至少能被人看见。4.2 数据访问层最小封装参数化查询与事务一起上数据库操作是业务系统里最不能靠运气的部分。我一般会封装一个最小的数据访问类把连接管理、参数化查询、事务三个能力锁在同一个类里Imports System.Data Imports System.Data.SqlClient Public Class DataAccess Private _connectionString As String Public Sub New(ByVal connectionString As String) _connectionString connectionString End Sub 查询单个值适合统计、判断存在性 Public Function ExecuteScalar( ByVal sql As String, ByVal ParamArray parameters() As SqlParameter) As Object Using conn As New SqlConnection(_connectionString) conn.Open() Using cmd As New SqlCommand(sql, conn) cmd.CommandTimeout 30 If parameters IsNot Nothing Then cmd.Parameters.AddRange(parameters) End If Return cmd.ExecuteScalar() End Using End Using End Function 事务内执行写入要么全部成功要么全部回滚 Public Function ExecuteInTransaction( ByVal sql As String, ByVal parameters() As SqlParameter) As Boolean Using conn As New SqlConnection(_connectionString) conn.Open() Dim tx As SqlTransaction conn.BeginTransaction(IsolationLevel.ReadCommitted) Try Using cmd As New SqlCommand(sql, conn, tx) If parameters IsNot Nothing Then cmd.Parameters.AddRange(parameters) End If cmd.ExecuteNonQuery() End Using tx.Commit() Return True Catch tx.Rollback() Throw End Try End Using End Function End Class调用方式Dim db As New DataAccess(server某服务器;database某库;user某用户;password某密码;) Dim p As New SqlParameter(orderId, SqlDbType.Int) p.Value 1001 Dim count As Object db.ExecuteScalar(SELECT COUNT(*) FROM orders WHERE order_idorderId, p)这个封装里有三个设计点值得讲。一是所有 SQL 一概用参数对象传值绝不拼字符串。拼字符串的问题不只是慢更是安全漏洞用户输入里带一个单引号就能改变 SQL 语义参数化查询让数据库把参数当数据而不当代码解析这一条直接决定系统敢不敢上线。二是两个方法都用 Using 包连接对象用完即还回连接池很多性能问题不是数据库慢是连接没释放连接池被占满后面的请求全部排队超时。三是事务方法里 Catch 之后 Rollback 再 Throw错误没被吞掉调用方能看到真实故障。4.3 三个必调参数超时、隔离级别、连接释放这一小节专门说参数怎么设。CommandTimeout 默认 30 秒报表型查询建议放宽到 60 或 120但不要无脑放大——超时设成 0 表示无限等待一条烂 SQL 能把整个系统拖死我见过某公司把超时改成 0 之后系统集体卡死的事故。连接字符串里 Pooling 默认开启Max Pool Size 默认 100。如果系统并发不高却频繁报连接池错误先查是不是哪里忘了关闭连接而不是调大连接池。隔离级别选择看业务容忍度给个简表隔离级别行为适合场景ReadCommitted读不到未提交数据但两次读可能不一致大多数业务系统默认选择Serializable范围锁并发最低库存、金额等强一致场景ReadUncommitted能读到脏数据性能最高报表、统计等容忍偏差的场景最后强调连接释放用 Using 是首选手写 Close 也要放在 Finally 里。很多人写到一半才想起要 Close异常路径上连接永远释放不了。宁可多用几层 Using 嵌套也别省这个括号。5. 高级 VB 编程避坑清单五个踩过才记得住的教训下面这几条全是真实项目里见过的翻车现场写成「现象—原因—解决」三段方便你遇到时直接对照。5.1 现象对象莫名变成 Nothing现象窗体关掉再打开控件状态错乱或者直接报 NullReferenceException也有的是对象用着用着就空了功能时好时坏。原因VB6 的窗体默认是全局单例第二次打开同一个窗体还是原来那个对象VB.NET 必须 New 才有新实例。老代码迁到 VB.NET 后习惯性地直接访问窗体变量拿到的是已经被释放的旧对象。另一个常见原因是事件订阅没退订对象被某个全局事件引用着Dispose 之后还在被调用。解决窗体访问统一走 New关闭时在 FormClosed 事件里把全局引用置空所有 AddHandler 在类 Dispose 时成对 RemoveHandler。访问任何可能已被释放的界面对象前先检查 IsDisposed这个习惯能救回大量调试时间。5.2 现象API 一调就崩溃或者返回乱码现象从网上抄的 API 声明编译能过一运行就崩或者字符串返回一堆问号。这是 Win32 API 调用里最常见的翻车方式。原因输出缓冲区用了 String 而不是 StringBuilder声明里没有 CharSet.Unicode句柄类型声明成 Integer 或 Long在 64 位系统上被截断。解决输出参数一律 StringBuilder 预分配声明统一 CharSet.Unicode句柄用 IntPtr。调用后检查返回值失败就用 Marshal.GetLastWin32Error() 取真实错误码别再凭感觉猜。网上抄声明一定要对照系统位数改类型。5.3 现象循环拼接字符串程序越来越慢现象处理几万行数据时内存暴涨、CPU 拉满速度肉眼可见地慢下去任务管理器里看内存曲线一路爬升。原因字符串是不可变对象每次 拼接都生成一个全新的字符串循环几万次就是几万次分配和复制旧对象排队等垃圾回收。解决循环内改用 StringBuilder.Append只需要拼一次的用 String.Format 或 String.Join。改完一般快一个数量级这是性价比最高的性能优化没有之一。5.4 现象数据库记录被莫名覆盖现象两个人同时改同一条订单后保存的人把先保存的修改覆盖了没有任何提示业务对不上账。原因UPDATE 语句无条件覆盖全部字段事务只能保证单条语句原子性管不住「先读后写」之间的并发窗口。解决表上加 Version 列时间戳或自增序号UPDATE 时在 WHERE 里带上次读到的版本号影响行数为 0 就说明数据已被别人改过这时候给用户提示而不是硬写UPDATE orders SET order_status newStatus, version version 1 WHERE order_id orderId AND version oldVersion如果影响行数为 0重新读取最新数据让用户决定是否覆盖。这是乐观并发的标准做法比锁表温和得多。5.5 现象换一台机器启动就崩溃现象开发机上一切正常部署到客户机器上一启动就报错或者某个功能点点了没反应。原因引用的第三方 COM 组件没注册目标机器上运行时版本不对程序是 32 位编译依赖的组件却是 64 位注册的或者反过来。解决发布清单里列清每个依赖组件启动时做环境自检检查关键组件是否存在缺什么提示什么打包前先确认目标机器的位数COM 组件按对应位数重新注册。这一条在部署现场能救回一整个下午的时间。6. 用反射写一个 VB 插件框架把高级技巧串成体系反射是多数 VB 开发者最后才碰的东西但它能把前面所有技巧串起来。场景某订单处理系统需要不断接入新的处理逻辑——归档、通知、对账如果每加一种就改主程序重新发布维护成本太高。常见做法是做一个插件目录主程序启动时扫描该目录下的 DLL用反射找到实现了约定接口的类型实例化后登记使用Public Interface IPlugin ReadOnly Property Name() As String Sub Execute() End Interface Public Class PluginHost Private _plugins As New List(Of IPlugin) Public Sub LoadPlugins(ByVal pluginFolder As String) For Each dllPath As String In IO.Directory.GetFiles(pluginFolder, *.dll) Try Dim asm As Reflection.Assembly Reflection.Assembly.LoadFrom(dllPath) For Each t As Type In asm.GetTypes() If t.IsClass AndAlso Not t.IsAbstract AndAlso GetType(IPlugin).IsAssignableFrom(t) Then Dim plugin As IPlugin DirectCast(Activator.CreateInstance(t), IPlugin) _plugins.Add(plugin) Console.WriteLine(已加载插件 plugin.Name) End If Next Catch ex As Exception 单个插件出错不能影响主程序启动 Console.WriteLine(跳过插件 dllPath ex.Message) End Try Next End Sub Public Sub RunAll() For Each p As IPlugin In _plugins p.Execute() Next End Sub End Class调用Dim host As New PluginHost() host.LoadPlugins(C:\plugins) host.RunAll()几个参数和边界条件值得说明。LoadPlugins 的文件夹参数建议用独立目录别把主程序的 DLL 混进去否则反射会尝试加载主程序自身徒增启动开销。IsAssignableFrom 判断类型是否实现 IPluginIsAbstract 排除抽象类。Activator.CreateInstance 反射创建实例等价于 New但类型在运行期才确定。每个 DLL 用 Try/Catch 单独包住一个坏插件只会跳过自己不影响主程序和其他插件启动。插件接口本身要定得稳接口一变所有已发布的插件都得跟着重新编译这是插件系统里最该提前想清楚的事。我后来养成的习惯是新写任何一个功能模块先问自己一句——这段逻辑以后会不会有第二种实现如果会就拆成接口如果不确定宁可先拆了再说回头合并比回头重构容易得多。这个习惯谈不上玄学就是被上面这些坑打磨出来的。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑