| 网站首页 | 业界新闻 | 小组 | 威客 | 人才 | 下载频道 | 博客 | 代码贴 | 在线编程 | 编程论坛
欢迎加入我们,一同切磋技术
用户名:   
 
密 码:  
共有 9108 人关注过本帖
标题:本人毕业设计 急求vb方面 中英文对照翻译文章
只看楼主 加入收藏
ioriliao
Rank: 7Rank: 7Rank: 7
来 自:广东
等 级:贵宾
威 望:32
帖 子:2829
专家分:647
注 册:2006-11-30
收藏
得分:0 

Tip 9: Trace the stack.
As you saw in the log file in Listing 1-3, we dump the VBA call stack when we hit an unexpected error because it can be useful for working out later what went wrong and why. We build an internal representation of VBA's stack (because VBA's stack is not actually available—shame), using two fundamental routines: TrTraceIn and TrTraceOut. Here they are in a typical routine:


Public Sub Testing()

' Set up general error handler.
On Error GoTo Error_General_Testing:
Const sProcSig = MODULE & " General.Testing"
Call TrTraceIn(sProcSig)

' ========== Body Code Starts ==========
.
.
.

' ========== Body Code Ends ==========

Call TrTraceOut(sProcSig)
Exit Sub

' Error handler.
Error_General_Testing:
.
.
.

End Sub

These routines are inserted by hand or by using the same internal tool I mentioned earlier in Tip 2 that adds line numbers to code. Notice that sProcSig is being passed into these routines so that the stack can be built containing the name of the module and the routine.

The stack frame object we use internally (not shown here) uses a Visual Basic collection with a class wrapper for its implementation. The class name we use is CStackFrame. As a prefix, C means class, and its single instance is named oStackFrame. We drop the o prefix if we're replacing a standard class such as Err or App.


/images/2011/147787/2011051411021524.jpg" border="0" />
2007-05-22 10:35
ioriliao
Rank: 7Rank: 7Rank: 7
来 自:广东
等 级:贵宾
威 望:32
帖 子:2829
专家分:647
注 册:2006-11-30
收藏
得分:0 

Tip 10: Use a ROOS (Resource Only OLE Server).
A basic ROOS (pronounced "ruse") is a little like a string table resource except that it runs in-process or out-of-process as an Automation server. A ROOS provides a structured interface to a set of objects and properties that enables us to build more flexible error handling routines.

For example, the ROOS holds a project's error constants (or rather the values mapped to the symbols used in the code that are resolved from the object's type library). The ROOS also holds a set of string resources that hold the actual error text for a given error and the methods used to load and process errors at run time. To change the language used in error reports or perhaps the vocabulary being used (for example, user vs. programmer), simply use a different ROOS. (No more DLLs with weird names!)

Tip 11: Replace useful intrinsic objects with your own.
Our main ROOS contains a set of alternative standard object classes, TMSErr and TMSApp, for example. These are instantiated as Err and App at application start-up as part of our application template initialization. (All our Visual Basic applications are built on this template.) By creating objects like this, we can add methods, properties, and so on to what looks like one of Visual Basic's own objects.

For example, our error object has extra methods named Push and Pop. These, mostly for historical reasons, are really useful methods because it's not clear in Visual Basic when Err.Clear is actually applied to the Err object—that is, when the outstanding error, which you've been called to handle, is automatically cleared. This can easily result in the reporting of error 0. Watch out for this because you'll see it a lot!

Usually, an error is mistakenly cleared in this way when someone is handling an error and from within the error handler he or she calls some other routine that causes Visual Basic to execute an Err.Clear. All sorts of things can make Visual Basic execute an Err.Clear. The result in this case is that the error is lost! These kinds of mistakes are really hard to find. They're also really easy to put in—lines of code that cause this to happen, that is!

The Help file under Err Object used to include this Caution about losing the error context.

If you set up an error handler using On Error GoTo and that handler calls another procedure, the properties of the Err object may be reset to zero and zero-length strings. To retain values for later use, assign the values of Err properties to variables before calling another procedure, or before executing Resume, On Error, Exit Sub, Exit Function, or Exit Property statements.

Of course, if you do reset Err.Number (perhaps by using On Error GoTo in the called routine), when you return to the calling routine the error will be lost. The answer, of course, is to preserve, or push, the error context onto some kind of error stack. We do this with Err.Push. It's the first line of code in the error handler—always. (By the way, Visual Basic won't do an Err.Clear on the call to Err.Push but only on its return—guaranteed.) Here's an example of how this push and pop method of error handling looks in practice:


Private Sub Command1_Click()

On Error GoTo error_handler:

VBA.Err.Raise 42

Exit Sub

error_handler:

Err.Push
Call SomeFunc
Err.Pop
MsgBox Err.Description
Resume Next

End Sub

Here we're raising an error (42, as it happens) and handling it in our error handler just below. The message box reports the error correctly as being an Application Defined Error. If we were to comment out the Err.Push and Err.Pop routines and rerun the code, the error information would be lost and the message box would be empty (as Err.Number and Err.Description have been reset to some suitable "nothing"), assuming the call to SomeFunc completes successfully. In other words, when we come to show the message box, there's no outstanding error to report! (The call to Err.Push is the first statement in the error handler. This is easy to check for during a code review.)


Note


--------------------------------------------------------------------------------

If we assume that Visual Basic itself raises exceptions by calling Err.Raise and that Err.Raise simply sets other properties of Err, such as Err.Number, our own Err.Number obviously won't be called to set VBA.Err properties (as it would if we simply had a line of code that read, say, Err.Number = 42). This is a pity because if it did call our Err.Number, we could detect (what with our Err.Number being called first before any other routines) that an error was being raised and automatically look after preserving the error context; that is, we could do an Err.Push automatically without having it appear in each error handler.

All sound good to you? Here's a sample implementation of a new Err object that contains Pop and Push methods:

In a class called ErrObject


Private e() As ErrObjectState

Private Type ErrObjectState

Description As String
HelpContext As Long
HelpFile As String
Number As Long

End Type

Public Property Get Description() As String

Description = VBA.Err.Description

End Property

Public Property Let Description(ByVal s As String)

VBA.Err.Description = s

End Property

Public Property Get HelpContext() As Long

HelpContext = VBA.Err.HelpContext

End Property

Public Property Let HelpContext(ByVal l As Long)

VBA.Err.HelpContext = l

End Property

Public Property Get HelpFile() As String

HelpFile = VBA.Err.HelpFile

End Property

Public Property Let HelpFile(ByVal s As String)

VBA.Err.HelpFile = s

End Property

Public Property Get Number() As Long

Number = VBA.Err.Number

End Property

Public Property Let Number(ByVal l As Long)

VBA.Err.Number = l

End Property

Public Property Get Source() As String

Source = VBA.Err.Source

End Property

Public Property Let Source(ByVal s As String)

VBA.Err.Source = s

End Property

Public Sub Clear()

VBA.Err.Clear

Description = VBA.Err.Description
HelpContext = VBA.Err.HelpContext
HelpFile = VBA.Err.HelpFile
Number = VBA.Err.Number

End Sub

Public Sub Push()

ReDim Preserve e(UBound(e) + 1) As ErrObjectState

With e(UBound(e))

.Description = Description
.HelpContext = HelpContext
.HelpFile = HelpFile
.Number = Number

End With

End Sub

Public Sub Pop()

With e(UBound(e))

Description = .Description
HelpContext = .HelpContext
HelpFile = .HelpFile
Number = .Number

End With

If UBound(e) Then
ReDim e(UBound(e) - 1) As ErrObjectState
Else
VBA.Err.Raise Number:=28 ' Out of stack space - underflow
End If

End Sub

Private Sub Class_Initialize()

ReDim e(0) As ErrObjectState

End Sub

Private Sub Class_Terminate()

Erase e()

End Sub

In Sub Main


Set Err = New ErrObject

In Global Module


Public Err As ErrObject

As you can see, our new Err object maintains a stack of a user-defined type (UDT) called ErrObjectState. An instance of this type basically holds information from the last error. In Sub Main we create our only ErrObject—note that it's called Err. This means that calls to methods like Err.Number will be directed to our object. In other words, Err refers to our instance of ErrObject and not the global instance VBA.Err. This means, of course, that we have to provide stubs for all the methods that are normally part of the global Err object: Number, Description, Source, and so on.

Note that we've left LastDLLError off the list. This is because when we pop the stack we'd need to write a value back into VBA.Err.LastDLLError and, unfortunately, this is a read-only property!

Another object we replace is the Debug object. We do this because we sometimes want to see what debug messages might be emitting from a built executable.

As you know, "normal" Debug.Print calls are thrown away by Visual Basic when your application is running as an executable; "special" Debug.Print calls, however, can be captured even when the application is running as an executable. Replacing this object is a little trickier than replacing the Err object because the Debug object name cannot be overloaded; that is,you have to call your new object something like Debugger. This new object can be designed to write to Visual Basic's Immediate window so that it becomes a complete replacement for the Debug object. Chapter 6 shows how you can write your own Assert method so that you can also replace the Debug object's Assert method.


/images/2011/147787/2011051411021524.jpg" border="0" />
2007-05-22 10:36
ioriliao
Rank: 7Rank: 7Rank: 7
来 自:广东
等 级:贵宾
威 望:32
帖 子:2829
专家分:647
注 册:2006-11-30
收藏
得分:0 

Tip 12: Check DLL version errors.
Debugging and defensive programming techniques can be used even in postimplementation. We always protect our applications against bad dynamic links (with DLLs and with ActiveX components such as OCXs) by using another internal tool. For a great example of why you should do this, see Chapter 8, Steve Overall's chapter about the Year 2000.

One of the really great things about Windows is that the dynamic linking mechanism, which links one module into another at run time, is not defined as part of some vendor's object file format but is instead part of the operating system itself. This means, for example, that it's really easy to do mixed language programming (whereas with static linking it's really hard because you're at the mercy of some vendor's linker—you just have to hope that it will understand). Unfortunately, this same mechanism can also get you into trouble because it's not until run time that you can resolve a link. Who knows what you'll end up linking to in the end—perhaps to an old and buggy version of some OCX. Oops!

By the way, don't use a GUID (explained below) to determine the version of any component. The GUID will almost always stay the same unless the object's interface has changed; it doesn't change for a bug fix or an upgrade. On the other hand, the object's version number should change whenever the binary image changes; that is, it should change whenever you build something (such as a bug fix or an interface change) that differs in any way from some previous version. The version number, not the GUID, tells you whether you're using the latest incarnation of an object or application.

Because it affects your application externally, this kind of versioning problem can be extremely hard to diagnose from afar (or for that matter, from anear).

So how do you check DLL version numbers? There are two ways of doing this since the underlying VERSIONINFO resource (which contains the version information) usually contains the information twice. A VERSIONINFO resource contains the version number both as a string and as a binary value. The latter is the most accurate, although the former is the one shown to you by applications like Microsoft Windows Explorer.

Here's an example. On my machine, OLEAUT32.DLL has a version number reported by Windows Explorer of 2.30.4261, whereas Microsoft System Information (MSINFO32.EXE version 2.51) shows the same file having a version number of 2.30.4261.1. Is Windows Explorer missing a .1 for some reason? Let's experiment further to find out. Here's another one—OPENGL32.DLL. (You can follow along if you have the file on your hard disk.) Windows Explorer reports this DLL as being version 4.00, yet Microsoft System Information says it's version 4.0.1379.1. Which is right? They both are, sort of. One version number is the string (4.00); the other is the binary (4.0.1379.1). As I mentioned earlier, the binary version number is more accurate, which is why it's used by the Windows' versioning API, Microsoft System Information, and of course all good installation program generators like InstallShield.


Globally Unique Identifiers (GUIDs)

A GUID (Globally Unique Identifier) is a 128-bit integer that can be used by COM (Component Object Model) to identify ActiveX components. Each GUID is guaranteed to be unique in the world. GUIDs are actually UUIDs (Universally Unique Identifiers) as defined by the Open Software Foundation's Distributed Computing Environment. GUIDs are used in Visual Basic mainly to identify the components you use in your projects (referenced under the References and Components items of the Project menu) and to help ensure that COM components do not accidentally connect to the "wrong" component, interface, or method even in networks with millions of component objects. The GUID is the actual name of a component, not the string you and I use to name it, or its filename. For example, a component we've probably all used before is F9043C88-F6F2-101A-A3C9-08002B2F49FB. You and I most likely refer to this component as the "Microsoft Common Dialog Control," or more simply, COMDLG32.OCX. (I have two of these on my machine, both with the same GUID. Their versions are different, however. One is 5.00.3112, and the other is 6.00.8169. Which do you link with?)

To determine a component's GUID, look in your project's VBP file. You'll see something like this if you use the Common Dialog control:


Object={F9043C88-F6F2-101A-A3C9-08002B2F49FB}#1.2#0;
COMDLG32.OCX
Visual Basic creates GUIDs for you automatically (for every ActiveX control you build). If you want to create them externally to Visual Basic, you can use either GUIDGEN.EXE or UUIDGEN.EXE, Microsoft utilities that come with the Visual C++ compiler, the ActiveX SDK, and on the Visual Basic 6 CD. You'll also find a Visual Basic program to generate GUIDs (in Chapter 7, my chapter on type libraries).

To see some sample code that determines a file's real version number, refer to the VB98\WIZARDS\PDWIZARD\SETUP1 source code. Everything you need is in there and ready for you to borrow!

By the way, when you compile your project, Visual Basic sets the string and binary version numbers to the number you enter on the Make tab of the Project Properties dialog box. For example, if you set your version number to, say, 1.2.3 in the Project Properties dialog box and build the EXE (or whatever) and then examine its version number using Windows Explorer and Microsoft System Information, you'll find that Windows Explorer reports the version number as 1.02.0003 while Microsoft System Information reports it as 1.2.0.3.

Backward Compatibility
Once you've got your version-checking code in place should you assume backward compatibility?

I'd say you should normally assume that 1.2.3 is simply a "better" 1.2.2 and so forth, although again I urge you to see Chapter 8 to find out what Steve has to say about OLEAUT32.DLL and to see a DLL that changed its functionality, not just its version number.

Normally a version number increment shows that the disk image has been altered, maybe with a bug fix. Bottom line: whenever the binary image changes, the version number should change also. An interface GUID change means that some interface has changed. Bottom line: whenever the interface changes, the GUID (and of course the version number) must change. If you don't have an interface, you're probably working with a DLL; in addition to being a binary file, this DLL has both entry points and behavior. Bottom line: if the behavior of a DLL changes or an entry point in it is modified, you must also change the filename.

To make the version numbers of your software even more accessible to your consumers, you might want to build an interface into your components, maybe called VERSIONINFO, that returns the version number of the EXE or DLL. All it would take is one Property Get:


Public Property Get VersionNumber() As String
VersionNumber = App.Major & "." & App.Minor & "." & App.Revision
End Property


/images/2011/147787/2011051411021524.jpg" border="0" />
2007-05-22 10:36
ioriliao
Rank: 7Rank: 7Rank: 7
来 自:广东
等 级:贵宾
威 望:32
帖 子:2829
专家分:647
注 册:2006-11-30
收藏
得分:0 

--------------------------------------------------------------------------------

Tip 13: Use Microsoft System Information (MSINFO32.EXE) when you can.
When you're trying to help a user with some problem (especially if you're in support), you often need to know a lot of technical stuff about the user's machine, such as what is loaded into memory or how the operating system is configured. Getting this information out of the user, even figuring out where to find it all in the first place, can be time-consuming and difficult. (Asking the user to continually hit Ctrl+Alt+Delete in an attempt to bring up the Task List and "see" what's running can be a dangerous practice: User: "Oh, my machine's rebooting." Support: "What did you do?" User: "What you told me to do—hit Ctrl+Alt+Delete again!") Microsoft thought so too, so they provided their users with an application to gather this information automatically: Microsoft System Information (MSINFO32.EXE). The good news is that you can use this application to help your customers.

Microsoft System Information comes with applications such as Microsoft Word and Microsoft Excel. If you have one of those applications installed, you're almost certain to have Microsoft System Information installed too. It also ships with Visual Basic 6. If you haven't seen this applet before, choose About Microsoft Visual Basic from the Help menu and click the System Info button. You'll see a window similar to Figure 1-5.

图片附件: 游客没有浏览图片的权限,请 登录注册

Figure 1-5 Running MSINFO32.EXE opens the Microsoft System Information application

The bottom line is that if your user is a Microsoft Office user or has an application such as Microsoft Excel installed, Microsoft System Information will be available. All you need to do then to provide the same information on the user's system is to run the same application!

To determine whether you've got this application to work with, look in the following location in the Registry:


HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Shared Tools\MSInfo\Path

In the following example, we use the registration API in ADVAPI32.DLL to retrieve the value of the Registry key. We can then check to see whether the application really exists. If it does, Shell it!

Declaration Section


Option Explicit

Private Const REG_SZ As Long = 1
Private Const ERROR_SUCCESS As Long = 0
Private Const HKEY_LOCAL_MACHINE As Long = &H80000002
Private Const STANDARD_RIGHTS_ALL As Long = &H1F0000
Private Const KEY_QUERY_VALUE As Long = &H1
Private Const KEY_ENUMERATE_SUB_KEYS As Long = &H8
Private Const KEY_NOTIFY As Long = &H10
Private Const SYNCHRONIZE As Long = &H100000
Private Const READ_CONTROL As Long = &H20000
Private Const STANDARD_RIGHTS_READ As Long = (READ_CONTROL)
Private Const KEY_READ As Long = _
((STANDARD_RIGHTS_READ _
Or KEY_QUERY_VALUE _
Or KEY_ENUMERATE_SUB_KEYS _
Or KEY_NOTIFY) _
And (Not SYNCHRONIZE))

Private Declare Function WinRegOpenKeyEx Lib "advapi32.dll" _
Alias "RegOpenKeyExA" (ByVal hKey As Long, _
ByVal lpSubKey As String, _
ByVal ulOptions As Long, _
ByVal samDesired As Long, _
phkResult As Long) As Long

Private Declare Function WinRegQueryValueEx Lib _
"advapi32.dll" Alias "RegQueryValueExA" _
(ByVal hKey As Long, _
ByVal lpValueName As String, _
ByVal lpReserved As Long, _
lpType As Long, lpData As Any, _
lpcbData As Long) As Long

Private Declare Function WinRegCloseKey Lib "advapi32" _
Alias "RegCloseKey" (ByVal hKey As Long) As Long

Form Load Event


Private Sub Form_Load()

Dim hKey As Long
Dim lType As Long
Dim Buffer As String

' Need some space to write string into - DLL routine
' expects us to allocate this space before the call.
Buffer = Space(255)

' Always expect failure!
cmdSystemInfo.Visible = False

' This will work if Microsoft System Information is installed.
If WinRegOpenKeyEx( _
HKEY_LOCAL_MACHINE _
, "SOFTWARE\Microsoft\Shared Tools\MSInfo" _
, 0 _
, KEY_READ _
, hKey _
) = ERROR_SUCCESS Then

' Read the Path value - happens to include the filename
' too, e.g.,
' "C:\Program Files\Common Files\Microsoft Shared\
' MSinfo\msinfo32.exe".
If WinRegQueryValueEx( _
hKey _
, "Path" _
, 0 _
, lType _
, ByVal Buffer _
, Len(Buffer) _
) = ERROR_SUCCESS Then
' Make sure we read a string back. If we did...
If lType = REG_SZ Then
' Make sure the Registry and reality are in
' alignment!
' Note: Using FileAttr() means you're
' suffering from paranoia<g>.
If Dir$(Buffer) <> "" Then
' Put the path into the button's Tag
' property and make the button visible.
cmdSystemInfo.Tag = Buffer
cmdSystemInfo.Visible = True
End If
End If

End If

' We open - we close.
Call WinRegCloseKey(hKey)

End If

End Sub

Button Click Event


Private Sub cmdSystemInfo_Click()

' If we got clicked, we must be visible and therefore
' must have our Tag property set to the name of the
' Microsoft System Information application - Shell it!
Call Shell(cmdSystemInfo.Tag, vbNormalFocus)

End Sub

In the code above, as the form loads (maybe this is an About box?) it detects whether or not Microsoft System Information exists. If it does, the form makes a command button visible and sets its Tag property to point to the program. When the form becomes visible, the button either will or won't be visible. If it is visible, you have Microsoft System Information on your machine. When you click the button, it simply calls Shell with the value in its Tag property. For more information on the APIs used in this example, see the appropriate Win32 documentation.

One of the neat little extras that came first with Visual Basic 5 was the little wizard dialog "thang" that allowed you to add standard dialog boxes to your application. One of these standard dialog boxes is an About dialog box. You'll notice that the About dialog box comes complete with a System Info button. The dialog box displays the Microsoft System Information utility using code similar to that shown above. (I think ours is cooler so I've left it here in the second edition.) This raises an interesting question, however. Is Microsoft implicitly giving you and me permission to ship MSINFO32.EXE (and anything that it needs) with an EXE? I'm afraid I don't know the answer to this one—sorry!



/images/2011/147787/2011051411021524.jpg" border="0" />
2007-05-22 10:38
ioriliao
Rank: 7Rank: 7Rank: 7
来 自:广东
等 级:贵宾
威 望:32
帖 子:2829
专家分:647
注 册:2006-11-30
收藏
得分:0 

Tip 14: Treat error handling like transaction processing.
When you hit an error, always attempt to bring the application back to a known and stable condition; that is, roll back from the error. To do this, you'll need to handle errors locally (to roll back within the scope of the erroring procedure) and more globally by propagating the error back up through each entry in the call chain.

Here's how you proceed. When your most local (immediate) error trap gets hit, make sure you clean up as required locally first. For example, make sure you close any files that you opened in this routine. Once that's done, and if this routine is not an event handler, reraise the error (in reality, you might raise some other error here) and repeat this process for each previous stack frame (a stack frame refers to an entry in the call chain); that is, continue this process for each preceding call until you get back up to an event handler. If you've cleaned up locally all the way through the call chain and if you had an error handler for each stack frame (so that you didn't jump over some routines), you should now have effectively rolled back from the error. It will seem as though the error never really happened. Note that by not reporting errors from anywhere other than an event handler, you will not have shown your user a stream of message boxes.

Localized error handling might need error handling itself. Look at the following code fragment:


On Error GoTo Error_Handler:

Dim nFile As Integer

nFile = FreeFile
Open "c:\time.txt" For Output Access Write As nFile
Print #nFile, Time$
Close nFile

Exit Sub

Error_Handler:

' Roll back!
Close nFile

Exit Sub

Imagine you have opened a file and are attempting to roll back in your error handler. How do you know whether or not you opened the file? In other words, did the error occur before or after the line of code that opens the file? If you attempt to close the file and it's not open, you'll cause an error—but if it's open, you don't want to leave it open as you're trying to roll back! I guess you could use Erl to determine where your code erred, but this implies that you're editing line numbered source code—yuck. (You'll recall from Tip 2 that we added line numbers only to the code for the final EXE, not to the code we're still editing.) Probably the best way to determine what did or did not get done is to limit the possibilities; that is, keep your routines small (so that you have only a small problem domain). Of course, that's not going to help us here. What we need to do is apply a little investigation!

Given this type of problem, you're probably going to have to test the file handle to see whether it points to an open file. In the code above, we would probably use FileAttr(nFile, 1) to determine whether or not the file nFile is open for writing. If the file is not open, FileAttr raises an exception (of course). And obviously, you can't handle this locally because you can't set an error trap from within an error trap unless your error handling is in another routine! (Refer to Tip 5 for details.)


/images/2011/147787/2011051411021524.jpg" border="0" />
2007-05-22 10:38
ioriliao
Rank: 7Rank: 7Rank: 7
来 自:广东
等 级:贵宾
威 望:32
帖 子:2829
专家分:647
注 册:2006-11-30
收藏
得分:0 

Tip 15: Don't test your own software or write your own test plans.
Do you have dedicated testers where you work? Possibly not—not many companies do. Many companies say they "can't afford such a luxury." Well, in my opinion, they're a luxury that's really worth it (as many of the leading software development companies in the world already know).

Independent testers should (and often do) exhibit the following characteristics:


Are impartial

Are less informed about the usage and the type of input your code expects

Are usually more knowledgeable about the usage and the type of input your code doesn't expect

Are more likely than you to spend time trying to break code

Are typically more leery of your interfaces and more critical of your coupling

Are into doing you damage and breaking your code

Are more informed than you about system limits

Unlike you, actually want to find bugs in your software.
From time to time, Microsoft talks about its ratio of developers to testers: around 1:1. You do the math; for every programmer there's a tester. In fact, rumor has it that some developers occasionally get shifted to being testers. This could happen if a developer consistently develops very buggy software. Nothing like a shift to testing to improve one's knowledge and appreciation of what good solid code involves.


/images/2011/147787/2011051411021524.jpg" border="0" />
2007-05-22 10:38
ioriliao
Rank: 7Rank: 7Rank: 7
来 自:广东
等 级:贵宾
威 望:32
帖 子:2829
专家分:647
注 册:2006-11-30
收藏
得分:0 

--------------------------------------------------------------------------------

Tip 16: Stress test your applications.
Years ago, the Windows SDK (Software Development Kit) shipped with an applet named SHAKER.EXE. This applet simply ran around allocating and releasing memory blocks. When and what it actually allocated or released was random!

What was it for, then? Well, before the days of protect mode and virtual memory addressing, you could access any arbitrary memory location through a simple pointer (using C as a programming language, of course). Often, and erroneously, these pointers would be stored in nonrefreshed static variables as an application yielded control to the operating system. This access—or similar access—would cause the problems for which SHAKER.EXE was used to try to uncover.

In between handling one event and a subsequent one, Windows could move (as it can now) both your code and data around. If you'd used a static pointer to, say, point to some data, you'd quickly discover that it was no longer pointing to what you intended. (Modern virtual addressing methods make this problem go away.) So what was the point of SHAKER.EXE? It turned out that, back then, even though your application was being naughty and had stored a static pointer, you didn't know it most of the time; the Windows memory manager hadn't moved your data around between your handling of two events. The bottom line was that you didn't really know you had a problem until memory moved, and on your machine, that rarely, if ever, happened. Customers, however, did see the problem because they were running both your application and others and had loaded their systems to a point that the memory manager was starting to move memory blocks around to accommodate everyone. The whole thing was like attempting to hold a party in a small closet. Initially, everyone had plenty of room. As more people arrived and the closet filled up, however, some of the guests were bound to get their feet stepped on sooner or later. SHAKER.EXE shook the operating system on the developer's machine until something fell off!

OK, so why the history lesson? Basically, the lesson is a good one and one we can still use. In fact, an associated application, named STRESS.EXE, still ships in Visual C++. (See Figure 1-6.)

图片附件: 游客没有浏览图片的权限,请 登录注册

Figure 1-6 Stress me (STRESS.EXE)

Like SHAKER.EXE, STRESS.EXE is used to make the operating system appear more loaded or busy than it actually is. For example, by using STRESS.EXE you can allocate all of your machine's free memory, making it look really loaded—or, reading from Tip 6 on, you can find out what happens when you run out of file handles.

Tools such as STRESS.EXE can present your code with a more realistic, perhaps even hostile, environment in which to work. Such conditions can cause many hidden problems to rise to the surface—problems you can fix at that point instead of later in response to a client's frustrated phone call. I'd certainly recommend using them.




/images/2011/147787/2011051411021524.jpg" border="0" />
2007-05-22 10:40
ioriliao
Rank: 7Rank: 7Rank: 7
来 自:广东
等 级:贵宾
威 望:32
帖 子:2829
专家分:647
注 册:2006-11-30
收藏
得分:0 

Tip 17: Use automated testing tools.
See Chapter 9, ""Well, at Least It Compiled OK!"" for coverage of this broad and very important subject.


--------------------------------------------------------------------------------

Tip 18: Consider error values.
Let's suppose you still want to return an indication of success from a function (instead of using exceptions). What values would you use to indicate whether or not something worked?

Normally, 0 (or False) is returned for failure, and -1 (True) for success. What are the alternatives? Some programmers like to return 0 for success and some other value for failure—the reason for failure is encoded in the value being returned. Other programmers prefer to return a negative value for failure that again encodes the reason.

By using the first alternative, we can quickly come up with some pretty weird-looking code:


If CreateThing() <> True Then ' It worked!

or


If Not CreateThing() Then ' It worked!

or


If CreateThing() = False Then ' It worked!

or


If CreateThing() = SUCCESS Then ' It worked!

SUCCESS, of course, is defined as 0. To capture failure, you can't just do the same, though:


If Not CreateThing() Then ' It worked!
Else
' Failed!
' What do we do?
End If

Here the reason for failure is lost. We need to hold it in some variable:


nResult = CreateThing()

If nResult <> SUCCESS Then
' Failed!
' What do we do?
End If

All very messy, especially where the language lacks the ability to do an assignment in a conditional expression (as is the case in Visual Basic and is not the case in C).

Consider someone writing the test using implicit expression evaluation:


If CreateThing() Then

If CreateThing works, it returns 0, which causes the conditional not to execute any code in the body of the compound statement. Yikes! Imagine what code fails to execute all because someone forgot to test against SUCCESS.

Because any nonzero value is evaluated as True (in an If), using a value other than 0 (say, a negative value) to indicate failure can be equally dangerous. Given that in any conditional expression you don't have to test against an explicit value and that nonzero means execute the compound statement, the language conspires against you here not to use 0 as a code indicating success.

I'd advise sticking to True meaning success and False meaning failure. In the case of failure, I'd implement a mechanism such as the one used in C (errno) or perhaps Win32's GetLastError. The latter returns the value of the last error (easily implemented in a project—you could even add a history feature or automatic logging of errors).


/images/2011/147787/2011051411021524.jpg" border="0" />
2007-05-22 10:41
ioriliao
Rank: 7Rank: 7Rank: 7
来 自:广东
等 级:贵宾
威 望:32
帖 子:2829
专家分:647
注 册:2006-11-30
收藏
得分:0 

--------------------------------------------------------------------------------

Tip 19: Tighten up Visual Basic's type checking.
Visual Basic itself doesn't always help you detect errors or error conditions. For example, consider the following code fragment:


Private Sub Fu(ByVal d As Date)
.
.
.

End Sub

Call Fu("01 01 98")

Is this code legal? If you ask around, quite often you'll find that developers say no, but it is perfectly legal. No type mismatch occurs (something that worries those who suspect this is illegal).

The reason the code is legal lies in Visual Basic itself. Visual Basic knows that the Fu procedure requires a Date type argument, so it automatically tries to convert the string constant "01 01 98" into a Date value to satisfy the call. If it can convert the string constant, it will. In other words, it does this kind of thing:


' The call .
.
.
'
' Call Fu("01 01 98")
'
' Equates to …
'
Const d As String = "01 01 98"

If IsDate(d) Then

Dim Local_d As Date

Local_d = CDate(d)

Call Fu(Local_d)

Else

Err.Raise Number:=13

End If

Now you see that Visual Basic can make the call by performing the cast (type coercion) for you. Note that you can even pass the argument by reference simply by qualifying the argument with the ByRef keyword, as in Call Fu(ByRef "01 01 98"). All you're passing by reference, in fact, is an anonymous variable that Visual Basic creates solely for this procedure call. By the way, all ByVal arguments in Visual Basic are passed by reference in this same fashion. That is, when it encounters a ByVal argument, Visual Basic creates an anonymous variable, copies the argument into the variable, and then passes a reference to the variable to the procedure. Interestingly, a variable passed by reference must be of the correct type before the call can succeed. (This makes perfect sense given that Visual Basic can trust itself to create those anonymous variables with the correct type; it can't trust user-written code to do the right thing, so Visual Basic has to enforce by-reference type checking strictly.)

So what's wrong with this automatic type coercion anyway? I hope you can see that the problem in the case above is that the cast is not helpful. We're passing an ambiguous date expression but receiving an actual, unambiguous date. This is because all date variables are merely offsets from December 30, 1899, and therefore unambiguous (for example, 1.5 is noon on December 31, 1899). There's no way "inside" of Fu to detect this fact and to refuse to work on the data passed. (Maybe that's how it should be? Maybe we should rely on our consumers to pass us the correct data type? No, I don't think so.)

One way to fix this [part of the] problem is to use Variants, which are some of the few things I normally encourage people to use. Have a look at this:


Call Fu("01 01 98")

Private Sub Fu(ByVal v As Variant)

Dim d As Date

If vbString = VarType(v) Then

If True = IsDate(CStr(v)) Then

If 0 = InStr(1, CStr(v), CStr(Year(CDate(v))), 1) Then
Err.Raise Number:=13
Else
d = CDate(v)
End If

End If

End If

' Use d here…

End Sub

The good thing about a Variant (and the bad?) is that it can hold any kind of data type. You can even ask the Variant what it's referencing by using VarType, which is very useful. Because we type the formal argument as Variant we'll receive in it a type equal to the type of the expression we passed. In the code above, VarType(v) will return vbString, not vbDate.

Knowing this, we can check the argument types using VarType. In the code above, we're checking to see if we're being passed a string expression. If the answer is yes, we're then checking to see that the string represents a valid date (even an ambiguous one). If again the answer is yes, we convert the input string into a date and then use InStr to see if the year in the converted date appears in the original input string. If it doesn't, we must have been passed an ambiguous date.

Here's that last paragraph rephrased and broken down a bit. Remember that a Date always holds an exact year because it actually holds an offset from December 30, 1899. Therefore, Year(a_Date_variable) will always give us back a full four-digit year (assuming that a_Date_variable represents a date after the year 999). On the other hand, the string that "seeds" the Date variable can hold only an offset—98 in the example. Obviously then, if we convert 98 to a Date (see Chapter 8 for more on this topic), we'll get something like 1998 or 2098 in the resulting Date variable. When converted to a string, those years are either "1998" or "2098"—neither of which appears in "01 01 98." We can say with some conviction, therefore, that the input string contains an ambiguous date expression, or even that its data type ("ambiguous date") is in error and will throw a type mismatch error.

All this date validation can be put inside a Validate routine, of course:


Private Sub Fu(ByVal v As Variant)

Dim d As Date

Call Validate(v, d)

' Use d here _ we don't get here if there's a problem with 'v'...

End Sub

In this Validate routine d is set to cast(v) if v is not ambiguous. If it is ambiguous, an exception is thrown. An exciting addition to this rule is that the same technique can also be applied to Visual Basic's built-in routines via Interface Subclassing.

How often have you wanted an Option NoImplicitTypes? I have, constantly. Here's how you can almost get to this situation:


Private Sub SomeSub()

MsgBox DateAdd("yyyy", 100, "01 01 98")

End Sub


Public Function DateAdd( _
ByVal Interval As String _
, ByVal Number As Integer _
, ByVal v As Variant _
)

Call Vali_Date(v)

DateAdd = VBA.DateTime.DateAdd(Interval, Number, CDate(v))

End Function


Private Sub Vali_Date(ByVal v As Variant)

' If 'v' is a string containing a valid date expression ...
If vbString = VarType(v) And IsDate(CStr(v)) Then

' If we've got a four digit year then we're OK,
' else we throw an err.
If 0 = InStr(1, CStr(v), _
Format$(Year(CDate(v)), "0000"), 1) Then
Err.Raise Number:=13
End If

End If

End Sub

In this code, the line MsgBox DateAdd(...) in SomeSub will result in a runtime exception being thrown because the date expression being passed is ambiguous ("01 01 98"). If the string were made "Y2K Safe"—that is, 01 01 1998—the call will complete correctly. We have altered the implementation of DateAdd; you could almost say we inherited it and beefed up its type checking.

Obviously this same technique can be applied liberally so that all the VBA type checking (and your own type checking) is tightened up across procedure calls like this. The really nice thing about doing this with Visual Basic's routines is that instead of finding, say, each call to DateAdd to check that its last argument is type safe, you can build the test into the replacement DateAdd procedure. One single replacement tests all calls. In fact, you can do this using a kind of Option NoImplicitTypes.

Use this somewhere, perhaps in your main module:


#Const NoImplicitTypes = True

Then wrap your validation routines appropriately:


Private Sub Vali_Date(ByVal v As Variant)

#If NoImplicitTypes = True Then

' If 'v' is…
If …
End If

#End If

End Sub


You now almost have an Option NoImplicitTypes. I say almost because we can't get rid of all implicit type conversions very easily (that's why I used "[part of the]" earlier). Take the following code, for example:


Dim d As Date

d = txtEnteredDate.Text

Your validation routines won't prevent d from being assigned an ambiguous date when txtEnteredDate.Text is "01 01 98", but at least you're closer to Option NoImplicitTypes than you would be without the routines.

Actually, at TMS we use a DateBox control, and even that control cannot stop this sort of use. (See Chapter 8 for more discussion about this, and see the companion CD for a demonstration.) A DateBox returns a Date type, not a Text type, and it's meant to be used like this:


Dim d As Date

d = dteEnteredDate.Date

Of course, it can still be used like this:


Dim s As String

s = dteEnteredDate.Date

Hmm, a date in a string! But at least s will contain a non-Y2K-Challenged date.

Might Microsoft add such an Option NoImplicitTypes in the future? Send them e-mail asking for it if you think it's worthwhile (mswish@microsoft.com).


A Not-Too-Small Aside into Smart Types, or "Smarties"

Another way to protect yourself against this kind of coercion is to use a smart type (we call these things Smarties, which is the name of a candy-coated confection) as an lvalue (the thing on the left-hand side of the assignment operator). A smart type is a type with vitamins added, one that can do something instead of doing nothing. The difference between smart types and "dumb" types is a little like the difference between public properties that are implemented using variables versus public properties implemented using property procedures. Here's some test code that we can feed back into the code above that was compromised:


Dim d As New iDate

d = txtEnteredDate.Text


Note that we're using a slightly modified version of the code here, in which d is defined as an instance (New) of iDate instead of just Date. (Of course, iDate means Intelligent Date.) Here's the code behind the class iDate:

In a class called iDate


Private d As Date

Public Property Get Value() As Variant

Value = CVar(d)

End Property

Public Property Let Value(ByVal v As Variant)

If vbDate = VarType(v) Then
d = CDate(v)
Else
Err.Raise 13
End If

End Property

OK then, back to the code under the spotlight. First you'll notice that I'm not using d.Value = txtEnteredDate.Text. This is because I've nominated the Value property as the default property. (Highlight Value in the Code window, select Procedure Attributes from the Tools menu, click Advanced >> in the Procedure Attributes dialog box, and then set Procedure ID to (Default).) This is the key to smart types, or at least it's the thing that makes them easier to use. The default property is the one that's used when you don't specify a property name. This means that you can do stuff like Print Left$(s, 1) instead of having to do Print Left$(s.Value, 1). Cool, huh? Here's that test code again:


Dim d As New iDate

d = txtEnteredDate.Text

If you bear in mind this implementation of an iDate, you see that this code raises a Type Mismatch exception because the Value Property Let procedure, to which the expression txtEnteredDate.Text is passed as v, now validates that v contains a real date. To get the code to work we need to do something a little more rigid:


Dim d As New iDate

d = CDate(txtEnteredDate.Text)


Just what the doctor ordered. Or, in the case of a date, does this perhaps make the situation worse? One reason why you might not want to explicitly convert the text to a date is that an ambiguous date expression in txtEnteredDate.Text is now converted in a way that's hidden from the validation code in the d.Value Property Let procedure. Perhaps we could alter the code a little, like this:


Public Property Let Value(ByVal v As Variant)

If vbString = VarType(v) And IsDate(CStr(v)) Then

' If we've got a four digit year then we're OK,
' else we throw an err.
If 0 = InStr(1, CStr(v), _
Format$(Year(CDate(v)), "0000"), 1) Then
Err.Raise Number:=13
End If

End If

d = CDate(v)

End Property

Here I've basically borrowed the code I showed earlier in this chapter which checks whether a date string is ambiguous. Now the following code works only if txtEnteredDate.Text contains a date like "01 01 1900":


Dim d As New iDate

d = txtEnteredDate.Text

Another cool thing about Smarties is that you can use them within an existing project fairly easily, in these different ways:


Add the class file(s) that implement your smart types.

Use search and replace to turn dumb types into Smarties.

Run your code and thoroughly exercise (exorcise) it to find your coercion woes.

Use search and replace again to swap back to dumb types (if you want).
Actually, I'll come clean here—it's not always this easy to use Smarties. Let's look at some pitfalls. Consider what happens when we search for As String and replace with As New iString. For one thing we'll end up with a few procedure calls like SomeSub(s As New iString), which obviously is illegal. We'll also get some other not-so-obvious—dare I say subtle?—problems.

Say you've got SomeSub(ByVal s As iString); you might get another problem here because now you're passing an object reference by value. ByVal protects the variable that you're passing so that it cannot be altered in a called procedure (a copy is passed and possibly altered in its place). The theory is that if I have s = Time$ in the called procedure, the original s (or whatever it was called in the calling procedure) still retains its old value. And it does; however, remember that the value we're protecting is the value of the variable. In our case that's the object reference, not the object itself. In C-speak, we can't change the object pointer, but because we have a copy of the pointer, we can access and change any of the object's properties. Here's an example that I hope shows this very subtle problem.

These two work the same:
Private Sub cmdTest_Click()

Dim s As New iString

s = Time$

Call SomeSub(s)

MsgBox s

End Sub

Sub SomeSub(ByRef s As iString)

s = s & " " & Date$

MsgBox s

End Sub


Private Sub cmdTest_Click()

Dim s As String

s = Time$

Call SomeSub(s)

MsgBox s

End Sub

Sub SomeSub(ByRef s As String)

s = s & " " & Date$

MsgBox s

End Sub


The assignment to s in both versions of SomeSub affects each instance of s declared in cmdTest_Click.

These two don't work the same:
Private Sub cmdTest_Click()

Dim s As New iString

s = Time$

Call SomeSub(s)

MsgBox s

End Sub

Sub SomeSub(ByVal s As iString)

s = s & " " & Date$

MsgBox s

End Sub


Private Sub cmdTest_Click()

Dim s As String

s = Time$

Call SomeSub(s)

MsgBox s

End Sub

Sub SomeSub(ByVal s As String)

s = s & " " & Date$

MsgBox s

End Sub



/images/2011/147787/2011051411021524.jpg" border="0" />
2007-05-22 10:41
ioriliao
Rank: 7Rank: 7Rank: 7
来 自:广东
等 级:贵宾
威 望:32
帖 子:2829
专家分:647
注 册:2006-11-30
收藏
得分:0 

The assignment to s in both versions of SomeSub affects each instance of s declared in cmdTest_Click.

These two don't work the same:
Private Sub cmdTest_Click()

Dim s As New iString

s = Time$

Call SomeSub(s)

MsgBox s

End Sub

Sub SomeSub(ByVal s As iString)

s = s & " " & Date$

MsgBox s

End Sub


Private Sub cmdTest_Click()

Dim s As String

s = Time$

Call SomeSub(s)

MsgBox s

End Sub

Sub SomeSub(ByVal s As String)

s = s & " " & Date$

MsgBox s

End Sub


The assignment to s in the SomeSub on the left still affects the instance of s declared in the cmdTest_Click on the left.

Let me again run through why this is. This happens because we're not passing the string within the object when we pass an iString; we're passing a copy of the object reference. Or, if you like, we're passing a pointer to the string. So it doesn't matter whether we pass the object by reference or by value—the called procedure has complete access to the object's properties.

You also cannot change iString to String in the procedure signature (if you did, you would defeat the purpose of all this, for one thing) and still pass ByRef because you're effectively trying to pass off an iString as a String, and you'll get a type mismatch.

Another area where you'll have problems is in casting (coercion). Consider this:


Private Function SomeFunc(s As iString) As iString

SomeFunc = s

End Function


Look OK to you? But it doesn't work! It can't work because = s, remember, means = s.Value—a String—and that's not an iString as implied by the assignment to SomeFunc. There's no way Visual Basic can coerce a String into an iString reference. (Maybe this is good because it's pretty strongly emphasized.) Could we coerce a String into an iString reference if we wrote a conversion operator (CiStr, for example)? Yes, but that would be overkill because we've already got a real iString in the preceding code. What we need to do is change the code to Set SomeFunc = s. Set is the way you assign an object pointer to an object variable. Anyway, it's simply a semantics change and so should be rejected out of hand. What we need is some way to describe to the language how to construct an iString from a String and then assign this new iString—not using Set—to the function name. (This is all getting us too close to C++, so I'll leave this well alone, although you might want to consider where you'd like Visual Basic to head as a language).

Anyway, you can see that this is getting messy, right? The bottom line is that you can do a good job of replacing dumb types with Smarties, but it's usually something that's best done right from the start of a project. For now, let's look at something that's easier to do on existing projects: another slant on type enforcement.

Type Checking Smarties How do you determine whether you're dealing with an As Object object or with a Smartie? Easy—use VarType. Consider this code; does it beep?


Dim o As New iInteger

If vbObjectiInteger = VarType(o) Then Beep

Normally all object types return the same VarType value (vbObject or 9), so how does VarType know about Smarties (assuming that vbObjectiInteger hasn't also been defined as 9)? Simple; see Tip 4. We subclass VarType and then add the necessary intelligence we need for it to be able to differentiate between ordinary objects and Smarties. For example, VarType might be defined like this


Public Function VarType(ByVal exp As Variant) _
As Integer ' vbVarType++

Select Case VBA.VarType(exp)

Case vbObject:

Select Case TypeName(exp)

Case "iInteger"
VarType = vbObjectiInteger

Case "iSingle"
VarType = vbObjectiSingle

Case Else
VarType = VBA.VarType(exp)

End Select

Case Else

VarType = VBA.VarType(exp)

End Select

End Function

The constants vbObjectiInteger, vbObjectiSingle, etc. are defined publicly and initialized on program start-up like this:


Public Sub main()

vbObjectiInteger = WinGlobalAddAtom(CreateGUID)
vbObjectiSingle = WinGlobalAddAtom(CreateGUID)
' Etc…

DoStartup

End Sub

WinGlobalAddAtom is an alias for the API GlobalAddAtom. This Windows API creates a unique value (in the range &HC000 through &HFFFF) for every unique string you pass it, and hopefully there will be no future clashes with whatever VarType will return. (So we have a variable constant: variable in that we don't know what GlobalAddAtom will return when we call it for the first time, but constant in that on subsequent calls GlobalAddAtom will return the same value it returned on the first call). It's basically a hash-table "thang." I want a unique value for each Smartie type I use, so I must pass a unique string to GlobalAddAtom. I create one of these by calling the CreateGUID routine documented in my Chapter 7, "Minutiae: Some Stuff About Visual Basic." This routine always returns a unique GUID string (something like C54D0E6D-E8DE-11D1-A614-0060806A9738), although in a pinch you could use the class name. The bottom line is that each Smartie will have a unique value which VarType will recognize and return!

Why not just use any old constant value? Basically I want to try to be less predictable (clashable with) and more unique, although one downside is this: because I cannot initialize a constant in this way, those vbObjectiInteger and others are variables and could be reassigned some erroneous values later in our code. Actually, that's a lie because they cannot be reassigned a new value. Why not? Because they're Smarties, too. To be precise, they're another kind of Smartie—Longs that can have one-time initialization only. (See Chapter 7 for the code that implements them.)

You might also want to consider whether to enforce at least strict type checking on procedure call arguments and set up some kind of convention within your coding standards whereby parameters are received as Variants (as outlined earlier), tested, and then coerced into a "correct" local variable of the desired type. Another advantage of this scheme is that it mandates a "fast pass by value" handling of arguments and thus can be used indirectly to reduce coupling. It's fast because it's actually a pass by reference!

In the following code, note that despite passing n to Fu by reference (which is the default passing mechanism, of course) we cannot alter it in Fu (if we're disciplined). This is because we work only in that routine on the local variable, d.

In a form (say):


Private Sub cmdTest_Click()

Dim n As Integer

n = 100

Call Fu(n)

End Sub


Public Sub Fu(vD As Variant)

Dim d As Single

d = IntegerToReal(vD)

' Use d safely ...

End Sub

In a testing module:


Public Function IntegerToReal(ByVal vI As Variant) As Double

#If True = NoImplicitTypes Then

Select Case VarType(vI)

Case vbInteger, vbLong:
IntegerToReal = CDbl(vI)

Case Else
Err.Raise VBErrTypeMismatch

End Select

#Else

IntegerToReal = CDbl(vI)

#End If

End Function

Here we're implying that our coding standards mandate some type checking. We're allowing integers (both Long and Short) to be implicitly coerced into either Singles or Doubles. Therefore, if we call Fu as Call Fu(100), we're OK. But if we call it as, say, Call Fu("100"), this will fail (if NoImplicitTypes is set to -1 in code using #Const, or in the IDE using the Project Properties dialog box). Note that d in Fu is defined as a Single but that IntegerToReal is returning a Double. This is always OK because an integer will always fit in to a Single; that is, we won't overflow here at all. To speed up the code, perhaps during the final build, you can simply define NoImplicitTypes as 0, in which case the routine forgoes type checking.

Of course, depending on your level of concern (or is that paranoia?), you can turn this up as much as you like. For instance, you could refuse to convert, say, a Long integer to a Single/Double. You're limited only to whatever VarType is limited to, meaning that you can detect any type as long as VarType does.


/images/2011/147787/2011051411021524.jpg" border="0" />
2007-05-22 10:43
快速回复:本人毕业设计 急求vb方面 中英文对照翻译文章
数据加载中...
 
   



关于我们 | 广告合作 | 编程中国 | 清除Cookies | TOP | 手机版

编程中国 版权所有,并保留所有权利。
Powered by Discuz, Processed in 0.031640 second(s), 8 queries.
Copyright©2004-2024, BCCN.NET, All Rights Reserved