wanshenmean
2026-03-13 4e60cd8649c3c19582ae560515cad00436587a2e
fix: improve MemoryStore code quality and error handling

Critical fixes:
- ConvertFromBytes now throws NotSupportedException for unsupported types instead of returning default
- Removed string type handling from generic methods (unreachable with struct constraint)
- Updated to use IsNullOrWhiteSpace instead of IsNullOrEmpty for address validation

Major fixes:
- ParseAddress now wraps FormatException/OverflowException with context
- Import now validates data lengths match region sizes before writing
- Added explicit validation that length is non-zero in ReadBytes
- Added comprehensive XML documentation for all public methods
- Documented GetRegion returns mutable references requiring lifecycle coordination
- Documented Export is expensive and should be used sparingly
- Documented DBRegion auto-creation behavior in Import/Export

Improvements:
- Fixed endianness handling to use big-endian consistently (S7 PLC standard)
- Added support for uint and byte types in conversion methods
- Improved error messages with detailed context
- Enhanced ExportDBRegion to handle missing DB blocks gracefully

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
已修改1个文件
264 ■■■■ 文件已修改
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Core/Memory/MemoryStore.cs 264 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Core/Memory/MemoryStore.cs
@@ -72,13 +72,21 @@
        /// <summary>
        /// 读取字节数据
        /// </summary>
        /// <param name="address">地址(如 "M100", "DB1.DBD0", "I0.0", "T1")</param>
        /// <param name="length">长度(必须大于0)</param>
        /// <returns>字节数组</returns>
        /// <exception cref="ArgumentException">地址为空或空白,或长度为0</exception>
        /// <exception cref="ObjectDisposedException">对象已释放</exception>
        public byte[] ReadBytes(string address, ushort length)
        {
            if (_disposed)
                throw new ObjectDisposedException(nameof(MemoryStore));
            if (string.IsNullOrEmpty(address))
                throw new ArgumentException("地址不能为空", nameof(address));
            if (string.IsNullOrWhiteSpace(address))
                throw new ArgumentException("地址不能为空或空白", nameof(address));
            if (length == 0)
                throw new ArgumentException("长度必须大于0", nameof(length));
            var (regionType, offset, dbNumber, bitOffset) = ParseAddress(address);
@@ -96,14 +104,21 @@
        /// <summary>
        /// 读取指定类型数据
        /// 支持的类型:bool, short, int, ushort, float
        /// 注意:string类型不受支持,请使用WriteBytes/ReadBytes或通过DBRegion直接访问
        /// </summary>
        /// <typeparam name="T">值类型(bool, short, int, ushort, float)</typeparam>
        /// <param name="address">地址</param>
        /// <returns>数据值</returns>
        /// <exception cref="ArgumentException">地址为空或空白,或类型不支持</exception>
        /// <exception cref="ObjectDisposedException">对象已释放</exception>
        public T Read<T>(string address) where T : struct
        {
            if (_disposed)
                throw new ObjectDisposedException(nameof(MemoryStore));
            if (string.IsNullOrEmpty(address))
                throw new ArgumentException("地址不能为空", nameof(address));
            if (string.IsNullOrWhiteSpace(address))
                throw new ArgumentException("地址不能为空或空白", nameof(address));
            var (regionType, offset, dbNumber, bitOffset) = ParseAddress(address);
@@ -122,13 +137,18 @@
        /// <summary>
        /// 写入字节数据
        /// </summary>
        /// <param name="address">地址</param>
        /// <param name="data">数据</param>
        /// <exception cref="ArgumentException">地址为空或空白</exception>
        /// <exception cref="ArgumentNullException">数据为null</exception>
        /// <exception cref="ObjectDisposedException">对象已释放</exception>
        public void WriteBytes(string address, byte[] data)
        {
            if (_disposed)
                throw new ObjectDisposedException(nameof(MemoryStore));
            if (string.IsNullOrEmpty(address))
                throw new ArgumentException("地址不能为空", nameof(address));
            if (string.IsNullOrWhiteSpace(address))
                throw new ArgumentException("地址不能为空或空白", nameof(address));
            if (data == null)
                throw new ArgumentNullException(nameof(data));
@@ -160,14 +180,21 @@
        /// <summary>
        /// 写入指定类型数据
        /// 支持的类型:bool, short, int, ushort, float
        /// 注意:string类型不受支持,请使用WriteBytes/ReadBytes或通过DBRegion直接访问
        /// </summary>
        /// <typeparam name="T">值类型(bool, short, int, ushort, float)</typeparam>
        /// <param name="address">地址</param>
        /// <param name="value">数据值</param>
        /// <exception cref="ArgumentException">地址为空或空白,或类型不支持</exception>
        /// <exception cref="ObjectDisposedException">对象已释放</exception>
        public void Write<T>(string address, T value) where T : struct
        {
            if (_disposed)
                throw new ObjectDisposedException(nameof(MemoryStore));
            if (string.IsNullOrEmpty(address))
                throw new ArgumentException("地址不能为空", nameof(address));
            if (string.IsNullOrWhiteSpace(address))
                throw new ArgumentException("地址不能为空或空白", nameof(address));
            var (regionType, offset, dbNumber, bitOffset) = ParseAddress(address);
@@ -198,7 +225,13 @@
        /// <summary>
        /// 获取内存区域
        /// 警告:返回的区域是可变的直接引用,调用者负责生命周期协调。
        /// 此方法主要用于高级场景和内部使用。一般情况下应使用Read/Write方法。
        /// </summary>
        /// <param name="regionType">区域类型(M/DB/I/Q/T/C)</param>
        /// <returns>内存区域接口</returns>
        /// <exception cref="ArgumentException">区域类型不支持</exception>
        /// <exception cref="ObjectDisposedException">对象已释放</exception>
        public IMemoryRegion GetRegion(string regionType)
        {
            if (_disposed)
@@ -219,6 +252,7 @@
        /// <summary>
        /// 清空所有内存
        /// </summary>
        /// <exception cref="ObjectDisposedException">对象已释放</exception>
        public void Clear()
        {
            if (_disposed)
@@ -234,7 +268,11 @@
        /// <summary>
        /// 导出内存数据
        /// 警告:这是一个昂贵的操作,会分配大量内存并复制所有区域数据。
        /// 建议仅在需要持久化或调试时使用。
        /// </summary>
        /// <returns>区域类型 -> 字节数组的字典</returns>
        /// <exception cref="ObjectDisposedException">对象已释放</exception>
        public Dictionary<string, byte[]> Export()
        {
            if (_disposed)
@@ -253,7 +291,13 @@
        /// <summary>
        /// 导入内存数据
        /// 会验证数据长度是否匹配区域大小,不匹配时会抛出异常。
        /// DB区支持自动创建块(如果写入时块不存在)。
        /// </summary>
        /// <param name="data">区域类型 -> 字节数组的字典</param>
        /// <exception cref="ArgumentNullException">数据为null</exception>
        /// <exception cref="ArgumentException">数据长度不匹配</exception>
        /// <exception cref="ObjectDisposedException">对象已释放</exception>
        public void Import(Dictionary<string, byte[]> data)
        {
            if (_disposed)
@@ -263,76 +307,115 @@
                throw new ArgumentNullException(nameof(data));
            if (data.ContainsKey("M"))
            {
                if (data["M"].Length != _config.MRegionSize)
                    throw new ArgumentException($"M区数据长度不匹配:期望{_config.MRegionSize},实际{data["M"].Length}");
                _mRegion.Write(0, data["M"]);
            }
            if (data.ContainsKey("DB"))
            {
                ImportDBRegion(data["DB"]);
            }
            if (data.ContainsKey("I"))
            {
                if (data["I"].Length != _config.IRegionSize)
                    throw new ArgumentException($"I区数据长度不匹配:期望{_config.IRegionSize},实际{data["I"].Length}");
                _iRegion.Write(0, data["I"]);
            }
            if (data.ContainsKey("Q"))
            {
                if (data["Q"].Length != _config.QRegionSize)
                    throw new ArgumentException($"Q区数据长度不匹配:期望{_config.QRegionSize},实际{data["Q"].Length}");
                _qRegion.Write(0, data["Q"]);
            }
            if (data.ContainsKey("T"))
            {
                var expectedTSize = _config.TRegionCount * 2; // 每个定时器2字节
                if (data["T"].Length != expectedTSize)
                    throw new ArgumentException($"T区数据长度不匹配:期望{expectedTSize},实际{data["T"].Length}");
                _tRegion.Write(0, data["T"]);
            }
            if (data.ContainsKey("C"))
            {
                var expectedCSize = _config.CRegionCount * 2; // 每个计数器2字节
                if (data["C"].Length != expectedCSize)
                    throw new ArgumentException($"C区数据长度不匹配:期望{expectedCSize},实际{data["C"].Length}");
                _cRegion.Write(0, data["C"]);
            }
        }
        /// <summary>
        /// 解析S7地址格式
        /// 支持格式: M100, DB1.DBD0, I0.0, Q0.0, T1, C1
        /// </summary>
        /// <param name="address">地址字符串</param>
        /// <returns>解析结果:区域类型、偏移量、DB编号(可选)、位偏移(可选)</returns>
        /// <exception cref="FormatException">地址格式无效时抛出,包含详细的上下文信息</exception>
        private (string regionType, ushort offset, ushort? dbNumber, byte? bitOffset) ParseAddress(string address)
        {
            address = address.Trim().ToUpper();
            // DB块地址: DB1.DBD0, DB1.DBW10, DB1.DBB20
            if (address.StartsWith("DB"))
            try
            {
                var parts = address.Split(new[] { '.' }, 2);
                var dbPart = parts[0]; // DB1
                var offsetPart = parts.Length > 1 ? parts[1] : null; // DBD0, DBW10, etc.
                var dbNumber = ushort.Parse(dbPart.Substring(2));
                ushort offset = 0;
                if (offsetPart != null)
                // DB块地址: DB1.DBD0, DB1.DBW10, DB1.DBB20
                if (address.StartsWith("DB"))
                {
                    // 解析偏移类型: DBD(4字节), DBW(2字节), DBB(1字节)
                    if (offsetPart.StartsWith("DBD"))
                        offset = ushort.Parse(offsetPart.Substring(3));
                    else if (offsetPart.StartsWith("DBW"))
                        offset = ushort.Parse(offsetPart.Substring(3));
                    else if (offsetPart.StartsWith("DBB"))
                        offset = ushort.Parse(offsetPart.Substring(3));
                    else
                        offset = ushort.Parse(offsetPart);
                    var parts = address.Split(new[] { '.' }, 2);
                    var dbPart = parts[0]; // DB1
                    var offsetPart = parts.Length > 1 ? parts[1] : null; // DBD0, DBW10, etc.
                    var dbNumber = ushort.Parse(dbPart.Substring(2));
                    ushort offset = 0;
                    if (offsetPart != null)
                    {
                        // 解析偏移类型: DBD(4字节), DBW(2字节), DBB(1字节)
                        if (offsetPart.StartsWith("DBD"))
                            offset = ushort.Parse(offsetPart.Substring(3));
                        else if (offsetPart.StartsWith("DBW"))
                            offset = ushort.Parse(offsetPart.Substring(3));
                        else if (offsetPart.StartsWith("DBB"))
                            offset = ushort.Parse(offsetPart.Substring(3));
                        else
                            offset = ushort.Parse(offsetPart);
                    }
                    return ("DB", offset, dbNumber, null);
                }
                return ("DB", offset, dbNumber, null);
            }
                // 带位地址: I0.0, Q0.0, M0.0
                if (address.Contains("."))
                {
                    var parts = address.Split('.');
                    var regionType = new string(parts[0].TakeWhile(char.IsLetter).ToArray());
                    var byteOffset = ushort.Parse(new string(parts[0].SkipWhile(char.IsLetter).ToArray()));
                    var bitOffset = byte.Parse(parts[1]);
            // 带位地址: I0.0, Q0.0, M0.0
            if (address.Contains("."))
                    return (regionType, byteOffset, null, (byte?)bitOffset);
                }
                // 定时器/计数器: T1, C1
                if (address.StartsWith("T") || address.StartsWith("C"))
                {
                    var regionType = address[0].ToString();
                    var number = ushort.Parse(address.Substring(1));
                    return (regionType, number, null, null);
                }
                // 普通地址: M100, I100, Q100
                var region = new string(address.TakeWhile(char.IsLetter).ToArray());
                var addr = ushort.Parse(new string(address.SkipWhile(char.IsLetter).ToArray()));
                return (region, addr, null, null);
            }
            catch (Exception ex) when (ex is FormatException || ex is OverflowException)
            {
                var parts = address.Split('.');
                var regionType = new string(parts[0].TakeWhile(char.IsLetter).ToArray());
                var byteOffset = ushort.Parse(new string(parts[0].SkipWhile(char.IsLetter).ToArray()));
                var bitOffset = byte.Parse(parts[1]);
                return (regionType, byteOffset, null, (byte?)bitOffset);
                throw new FormatException($"地址格式无效: '{address}'。{ex.Message}", ex);
            }
            // 定时器/计数器: T1, C1
            if (address.StartsWith("T") || address.StartsWith("C"))
            {
                var regionType = address[0].ToString();
                var number = ushort.Parse(address.Substring(1));
                return (regionType, number, null, null);
            }
            // 普通地址: M100, I100, Q100
            var region = new string(address.TakeWhile(char.IsLetter).ToArray());
            var addr = ushort.Parse(new string(address.SkipWhile(char.IsLetter).ToArray()));
            return (region, addr, null, null);
        }
        /// <summary>
@@ -344,13 +427,15 @@
            var data = _mRegion.Read(offset, (ushort)size);
            if (typeof(T) == typeof(bool))
                return default; // M区不支持bool读取,应该用Mx.x格式
                throw new NotSupportedException("M区不支持bool读取,应该使用Mx.x格式(位地址)");
            return ConvertFromBytes<T>(data);
        }
        /// <summary>
        /// 读取DB区数据
        /// 注意:string类型不受此泛型方法支持(受where T : struct约束)
        /// 请使用WriteBytes/ReadBytes或通过DBRegion直接访问字符串
        /// </summary>
        private T ReadDBRegion<T>(ushort dbNumber, ushort offset) where T : struct
        {
@@ -360,7 +445,8 @@
                "Int16" => (T)(object)_dbRegion.ReadInt(dbNumber, offset),
                "Int32" => (T)(object)_dbRegion.ReadDInt(dbNumber, offset),
                "Single" => (T)(object)_dbRegion.ReadReal(dbNumber, offset),
                "String" => (T)(object)_dbRegion.ReadString(dbNumber, offset, 254),
                // String类型在泛型约束下不可达,但保留注释说明
                // "String" => (T)(object)_dbRegion.ReadString(dbNumber, offset, 254),
                _ => ConvertFromBytes<T>(_dbRegion.Read(dbNumber, offset, (ushort)System.Runtime.InteropServices.Marshal.SizeOf<T>()))
            };
        }
@@ -418,6 +504,8 @@
        /// <summary>
        /// 写入DB区数据
        /// 注意:string类型不受此泛型方法支持(受where T : struct约束)
        /// 请使用WriteBytes/ReadBytes或通过DBRegion直接访问字符串
        /// </summary>
        private void WriteDBRegion<T>(ushort dbNumber, ushort offset, T value) where T : struct
        {
@@ -435,9 +523,10 @@
                case "Single":
                    _dbRegion.WriteReal(dbNumber, offset, (float)(object)value);
                    break;
                case "String":
                    _dbRegion.WriteString(dbNumber, offset, (string)(object)value, 254);
                    break;
                // String类型在泛型约束下不可达,但保留注释说明
                // case "String":
                //     _dbRegion.WriteString(dbNumber, offset, (string)(object)value, 254);
                //     break;
                default:
                    var data = ConvertToBytes(value);
                    _dbRegion.Write(dbNumber, offset, data);
@@ -493,7 +582,9 @@
        /// <summary>
        /// 从字节数组转换为值类型
        /// 使用大端字节序(Big-Endian),与S7 PLC规范一致
        /// </summary>
        /// <exception cref="NotSupportedException">类型不支持时抛出</exception>
        private T ConvertFromBytes<T>(byte[] data) where T : struct
        {
            if (typeof(T) == typeof(short))
@@ -505,15 +596,31 @@
            if (typeof(T) == typeof(ushort))
                return (T)(object)((ushort)((data[0] << 8) | data[1]));
            if (typeof(T) == typeof(float))
                return (T)(object)BitConverter.ToSingle(data, 0);
            if (typeof(T) == typeof(uint))
                return (T)(object)((uint)((data[0] << 24) | (data[1] << 16) | (data[2] << 8) | data[3]));
            return default;
            if (typeof(T) == typeof(float))
            {
                // BitConverter默认使用系统字节序,需要转换为大端
                if (BitConverter.IsLittleEndian)
                    Array.Reverse(data);
                return (T)(object)BitConverter.ToSingle(data, 0);
            }
            if (typeof(T) == typeof(bool))
                return (T)(object)(data[0] != 0);
            if (typeof(T) == typeof(byte))
                return (T)(object)data[0];
            throw new NotSupportedException($"不支持的类型: {typeof(T).Name}。支持的类型:bool, byte, short, int, ushort, uint, float");
        }
        /// <summary>
        /// 将值类型转换为字节数组
        /// 使用大端字节序(Big-Endian),与S7 PLC规范一致
        /// </summary>
        /// <exception cref="NotSupportedException">类型不支持时抛出</exception>
        private byte[] ConvertToBytes<T>(T value) where T : struct
        {
            if (typeof(T) == typeof(short))
@@ -530,37 +637,64 @@
            if (typeof(T) == typeof(ushort))
                return new[] { (byte)((ushort)(object)value >> 8), (byte)((ushort)(object)value & 0xFF) };
            if (typeof(T) == typeof(uint))
                return new[] {
                    (byte)((uint)(object)value >> 24),
                    (byte)(((uint)(object)value >> 16) & 0xFF),
                    (byte)(((uint)(object)value >> 8) & 0xFF),
                    (byte)((uint)(object)value & 0xFF)
                };
            if (typeof(T) == typeof(float))
                return BitConverter.GetBytes((float)(object)value);
            {
                var bytes = BitConverter.GetBytes((float)(object)value);
                // BitConverter默认使用系统字节序,需要转换为大端
                if (BitConverter.IsLittleEndian)
                    Array.Reverse(bytes);
                return bytes;
            }
            if (typeof(T) == typeof(bool))
                return new[] { (byte)((bool)(object)value ? 1 : 0) };
            throw new NotSupportedException($"不支持的类型: {typeof(T).Name}");
            if (typeof(T) == typeof(byte))
                return new[] { (byte)(object)value };
            throw new NotSupportedException($"不支持的类型: {typeof(T).Name}。支持的类型:bool, byte, short, int, ushort, uint, float");
        }
        /// <summary>
        /// 导出DB区数据
        /// 导出所有DB块的连续数据
        /// 注意:DB区支持自动创建块,导出时只包含已创建的块
        /// </summary>
        private byte[] ExportDBRegion()
        {
            // 简化版本:导出所有DB块的连续数据
            // 实际实现可能需要更复杂的序列化格式
            var result = new List<byte>();
            for (ushort i = 1; i <= _config.DBBlockCount; i++)
            {
                var blockData = _dbRegion.Read(i, 0, (ushort)_config.DBBlockSize);
                result.AddRange(blockData);
                try
                {
                    var blockData = _dbRegion.Read(i, 0, (ushort)_config.DBBlockSize);
                    result.AddRange(blockData);
                }
                catch (ArgumentException)
                {
                    // DB块不存在,跳过
                    // 填充0以保持连续性
                    result.AddRange(new byte[_config.DBBlockSize]);
                }
            }
            return result.ToArray();
        }
        /// <summary>
        /// 导入DB区数据
        /// 按顺序导入所有DB块
        /// 注意:DB区支持自动创建块(如果写入时块不存在)
        /// </summary>
        private void ImportDBRegion(byte[] data)
        {
            // 简化版本:按顺序导入所有DB块
            int offset = 0;
            for (ushort i = 1; i <= _config.DBBlockCount && offset < data.Length; i++)
            {