什么架构来解决这个SystemOutOfMemoryException,同时允许我实例化一个工作表的单元格?
概要
这个问题是后续build设一个简单的电子表格API的愿望,同时保持对那些谁了解Excel的用户友好。
总结起来,这个问题与以下两点有关:
1. 如何从索引中实现列自我命名? ;
2. 如何使这个自定义工作表初始化更快? 。
目的
提供一个简化的Excel API,用作包含Application
, Workbook
, Worksheet
和Range
类/接口等内核组件的封装,同时仅公开每个对象的最常用对象属性。
用法示例
这个用法的例子是从unit testing中得到灵感的,这个unit testing使我能够把这个解决scheme提升到现在的地位。
Dim file as String = "C:\Temp\WriteTest.xls" Using mgr As ISpreadsheetManager = New SpreadsheetManager() Dim wb as IWorkbook = mgr.CreateWorkbook() wb.Sheets("Sheet1").Cells("A1").Value = 3.1415926 wb.SaveAs(file) End Using
现在我们打开它:
Dim file as String = "C:\Temp\WriteTest.xls" Using mgr As ISpreadsheetManager = New SpreadsheetManager() Dim wb as IWorkbook = mgr.OpenWorkbook(file) // Working with workbook here... End Using
讨论
实例化Excel工作簿时:
- Worksheet的实例在Workbook.Sheets集合中自动初始化;
- 在初始化时,Worksheet通过可以表示一个或多个单元格的
Range
对象初始化其单元。
只要工作表存在,这些单元格立即可用其所有属性进行访问。
我希望重现这种行为
- Workbook类构造函数用原生表单初始化Workbook.Sheets集合属性;
- Worksheet类构造函数使用本机单元初始化Worksheet.Cells集合属性。
我的问题来自工作表类的构造函数,同时初始化在#2说明的Worksheet.Cells集合属性。
思考
遇到以上这些相关问题遇到的问题,我想弄清楚另一个架构,可以让我:
- 访问单元格的特定function需要时的
Range
; - 通过我的
ICell
接口提供最常用的属性; - 从其初始化访问工作表的所有
Range
单元格。
请记住,访问Range.Value
属性是使用Interop与底层Excel应用程序实例进行最快速的交互。
所以,我想用单元格名称初始化ReadonlyOnlyDictionary(Of String, ICell)
,而不立即包装Range
接口的实例,这样我就可以简单地生成行和列索引以及单元格的名称来索引我的字典,那么只有在想要访问或格式化特定单元格或单元格区域时,才能分配Cell.NativeCell
属性。
这样,字典中的数据将被索引,从Worksheet
类构造函数中生成的列索引中获取单元格的名称。 那么,当一个人会这样做:
Using mgr As ISpreadsheetManager = New SpreadsheetManager() Dim wb As IWorkbook = mgr.CreateWorkbook() wb.Sheet(1).Cells("A1").Value = 3.1415926 // #1: End Using
#1:这将允许我使用我的Cell
类中的索引将给定的值写入特定的单元格,这比使用其名称直接针对Range
更快。
问题和疑虑
此外,在使用UsedRange.get_Value()
或Cells.get_Value()
,将返回Object(,)数组。
所以我应该只对使用Object(,)
数组的单元格感到高兴,而不能以某种方式对其进行格式化?
2.如何构build这些Worksheet和Cell类,以便在处理Object(,)
数组时提供最佳性能,同时保持Cell实例可能表示或包装单个单元格Range的可能性?
感谢您花时间阅读我的信息,并对那些回答的人表示由衷的谢意。
所使用的体系结构经历了一个名为CellCollection
的对象类。 以下是它的作用:
基于这些假设:
鉴于Excel工作表有256列和65536行,
鉴于16,777,216(256 * 65536)个单元需要一次实例化,
考虑到工作表中最常见的用途less于1000行,less于100列;
鉴于我需要它能够引用与他们的地址(“A1”)的细胞; 和
考虑到它的基准是一次访问所有值并将它们加载到内存中的
object[,]
作为使用底层Excel工作表的最快方法*
我已经考虑不实例化任何单元格,让我的IWorksheet
接口中的CellCollection
属性初始化,实例化时,除现有的工作簿外,为空。 因此,在打开工作簿时,我validation了NativeSheet.UsedRange
为空或返回null(在Visual Basic中为Nothing),否则,我已经在内存中获得了使用的“本机单元格”,因此只能将它们添加到我的内部CellCollection
字典,同时索引他们各自的地址。
最后, 懒惰的初始化devise模式来拯救! =)
public class Sheet : ISheet { public Worksheet(Microsoft.Office.Interop.Excel.Worksheet nativeSheet) { NativeSheet = nativeSheet; Cells = new CellCollection(this); } public Microsoft.Office.Interop.Excel.Worksheet NativeSheet { get; private set; } public CellCollection Cells { get; private set; } } public sealed class CellCollection { private IDictionary<string, ICell> _cells; private ReadOnlyDictionary<string, ICell> _readonlyCells; public CellCollection(ISheet sheet) { _cells = new Dictionary<string, ICell>(); _readonlyCells = new ReadonlyDictionary<string, ICell>(_cells); Sheet = sheet; } public readonly ReadOnlyDictionary<string, ICell> Cells(string addresses) { get { if (string.IsNullOrEmpty(addresses) || 0 = address.Trim().Length) throw new ArgumentNullException("addresses"); if (!Regex.IsMatch(addresses, "(([A-Za-z]{1,2,3}[0-9]*)[:,]*)")) throw new FormatException("addresses"); foreach(string address in addresses.Split(",") { Microsoft.Office.Interop.Excel.Range range = Sheet.NativeSheet.Range(address) foreach(Microsoft.Office.Interop.Excel.Range cell in range) { ICell c = null; if (!_cells.TryGetValue(cell.Address(false, false), c)) { c = new Cell(cell); _cells.Add(c.Name, c); } } } return _readonlyCells; } } public readonly ISheet Sheet { get; private set; } }
显然,这是第一次尝试,迄今为止工作得很好,性能可以接受。 虽然谦虚,但我觉得它可以使用一些优化,虽然我现在会这样使用它,如果需要,可以稍后优化它。
写完这个集合之后,我能够达到预期的行为。 现在,我将尝试实现一些.NET接口,使其可用于某些IEnumerable
, IEnumerable<T>
, ICollection
, ICollection<T>
等,以便它们可以分别被视为一个真正的.NET集合。
随意发表评论,并为此代码带来build设性的替代scheme和/或更改,以便它可能变得比现在更大。
我希望有一天能达到目的。
谢谢阅读! =)