Chapter 4 Interface
Not long ago, a friend engaged in software gave me a riddle. Face-to-face is a "Blind Date", which makes me guess a software term. After about a minute, I guess the answer is "Object-Oriented ". I think it's quite interesting. I thought about a riddle to return to him. The face was a "kiss", which also gave him a guess of the software term. One minute later, he humorously said, "When you are targeting your beautiful objects, you can't help but contact her !". We laughed at the same time. Talking and laughing, it seems that we have a deep feeling with our own programs. For us, software is life.
Section 1 interface concepts
The term "interface" is too broad and may cause misunderstanding. The interface we mentioned here is not a discussion of the program interface in the modular design of the program, or an interface between computer hardware devices. The interface we want to talk about now is a program language concept similar to the class, and it is also the basic technology for implementing distributed object software.
In Delphi, interfaces are defined in the same way as classes, but not reserved words, but interfaces. Although interfaces and classes have similar definitions, their meanings are quite different.
Classes are abstract descriptions of objects with the same attributes and behaviors. Class description is for objects in the real world. Interfaces do not describe objects, but describe behaviors. An interface is a description of a behavior method, regardless of whether it is an object or something else. Therefore, interfaces and classes start from different points of view.
It can be said that interfaces are a pure technical concept in the Development of Cross-process or distributed programming technology. The concept of class is a universal way of thinking and the core of Object-oriented thinking. However, the concept of interfaces is indeed developed along with the object-oriented software idea. Understanding and constructing a cross-process or distributed software structure using the interface concept is more intuitive and simple than low-level concepts such as Remote Procedure Call (RPC) that were directly used in the early stage. This is because you can understand an interface just like an object, and do not care whether the object is local or remote.
In Delphi, an interface is declared as an interface. The naming principle is that all interfaces are named after the letter I, just as all classes start with the letter T. You can only define methods in the declaration of interfaces, but not data members. Because the interface only describes the method and behavior of the other party, and does not store the attribute status of the object. Although attributes can be defined for interfaces in Delphi, these attributes must be accessed based on methods.
All interfaces are inherited directly or indirectly from iunknown. Iunknown is the original ancestor of all Interface Types and has the same status as tobject in the class concept. The saying "one interface inherits another interface" is actually not correct, because it should be "one interface expands another interface ". The expansion of interfaces is a kind of "compatibility". This kind of "compatibility" is single, and there will never be an interface compatible with both parent interfaces at the same time.
Because the interface only describes a set of methods and actions, the implementation of these methods and actions must depend on the class. An interface cannot create an instance. There is no interface instance at all. Only a class can create an object instance. However, there must be an object instance behind an interface. This object is the implementer of the interface method, and the interface is a reference to a group of methods of this object.
Conceptually, an object class can implement one or more interfaces. Class is only responsible for implementing interfaces, rather than inheriting one or more interfaces. The word "Implementation" and the word "inheritance" have different meanings and should be distinguished from each other in terms of concept.
Generally, a guid that uniquely identifies an interface type is required when an interface is declared. The interface type is used by programs distributed in different process spaces or computers. Unlike classes, the interface type is only identified and used in one program space. To ensure that an interface type can be uniquely identified anywhere, you must use a method that effectively identifies different interfaces. Manual naming is not acceptable. No one can ensure that the interfaces you develop will not be renamed with others. Therefore, a so-called GUID (globally unique identifier) came into being. It is a random identifier generated by a complex algorithm, with 16 bytes in length, which can ensure that the IDS generated worldwide and locally are different. In the editing environment of Delphi, you can use Ctrl + Shift + G to easily generate a guid as the unique identifier of the interface.
It is necessary to specify a guid for the interface. Although the guid of an interface can be compiled without being specified, some functions related to interface identification and conversion may be problematic. In particular, in the development of com-based programs, guid must be indispensable.
The interface concept is actually very simple, but it plays a key role in distributed software development. Some friends think that interfaces are complicated because they do not understand the concept and principle of interfaces. Because people are always mysterious about what they don't know. This mysterious feeling will often lead to fear of the unknown world. To unveil the secrets of interfaces, you must constantly learn and understand the mysteries of interfaces. In fact, there will be a lot of fun in the process of exploration, right.
Section 2 iunknown
Because iunknown is the common ancestor of all interfaces, you must first understand it. Knowing the cause of a thing can effectively help us understand the process and result of the thing. The original definition of iunknown is in the system. Pas unit. Because it is defined in the system. Pas unit, it must be the original thing related to the system or compiler. Let's take a look at the definition of iunknown, which is very simple. There are only 6 lines in total.
Iunknown = interface
['{0000-0000-0000-c000-000000000046}']
Function QueryInterface (const IID: tguid; out OBJ): hresult; stdcall;
Function _ addref: integer; stdcall;
Function _ release: integer; stdcall;
End;
However, these six lines of code define the foundation of the interface world. The three interface methods contain simple, profound, and profound philosophies. Understanding these philosophies will benefit us a lot in programming interface-based programs.
The three interface methods of iunknown are required for each interface object class and are the basic methods of the interface mechanism. Why are these three methods the basis of the interface mechanism? Let me hear it.
First, let's talk about the QueryInterface interface method. We know that an object class can implement multiple interfaces. Any interface object must implement the iunknown interface. Therefore, if you get an interface pointer, you can call the QueryInterface method through this interface pointer. You can call QueryInterface to know what interfaces are implemented by this interface pointer. This is very important for the interface programming mechanism. Determine whether an interface pointer implements an interface function. Interface matching and conversion between different interface types are related to the QueryInterface method.
QueryInterface has two parameters and one return value. The first parameter is the interface type identifier, that is, a 16-byte guid identifier. Since the Delphi compiler knows the guid corresponding to each interface, you can directly use an identifier such as imyinterface as the first parameter. If this interface supports the interface type specified by the first parameter, the obtained interface pointer is sent back to the calling program through the second parameter OBJ, and the returned value is s_ OK.
It can be seen from this that it is necessary to specify a guid for the interface. Because the QueryInterface method requires such an identifier, and it serves as the basis for interfaces and matching and conversion mechanisms.
Next, let's talk about the _ addref and _ release interface methods. The _ addref and _ release interface methods are required for each interface object class. _ Addref increases the reference count for this interface object, while _ release reduces the reference for this interface object. If the reference count of an interface object is zero, delete the interface object and release the space. This is a basic principle required by the interface mechanism. It is like a simple principle like 1 + 1 = 2 and does not require profound explanation. Mathematicians are interested in studying why one plus one equals two? However, mathematicians have a thorough understanding of 1 + 1 = 2. Similarly, a deep understanding of the interface object reference mechanism will give us a lot of truth, which will benefit our development work.
A master once said: interfaces are counted references!
To understand this sentence, we must first understand the concept of "reference. "Reference" means "borrow", indicating a reference relationship. The referenced party only finds the contact between the referenced party, And the referenced party is the true center. Because the object can be found through this reference relationship, the reference actually represents the identity of the object. In program design, reference is actually a pointer, which is represented by the object address.
In a program that is not based on the interface mechanism, you do not need to manage the reference relationship of the object. Because instances of non-interface objects are in the same process space, you can use programs to strictly control the creation, use, and release of objects. However, in an interface-based program, object creation, use, and release may occur in the same process space or in different process spaces, even the two computers on the Internet are thousands of miles away. When an interface is created in one place, the object implementing this interface may exist in another place. After an interface is created in one place, it may be used in another place. In this case, it is very difficult to use traditional programs to control the creation and release of objects. There must be another agreed mechanism to deal with the establishment and release of objects. Therefore, this important task falls into the iunknown _ addref and _ release.
This interface object reference mechanism requires that the creation and release of interface objects are the responsibility of the program where the object instance is located, that is, the object class that implements the interface. When you reference the interface of this object anywhere, you must call the _ addref method of the interface. When this object is no longer referenced, The _ release method of the interface must also be called. Once an object instance is found to be no longer referenced, it is released.
To solve the space management problem of interface object instances, the _ addref and _ release methods must be implemented by all interface object classes.
Section 3 Life and Death of interface objects
The title of this section seems a little scary. How can an interface object be linked to life and death? Is the life and death of interface objects really so important? A good ruler should care about the life and death of the people, and a good programmer should also care about the life and death of objects. The interface objects are wandering in the distributed network, so we should be more concerned about their life and death!
Because the interface object is created along with the generation of interface references, it also disappears along with the completion of interface references. When using interfaces in Delphi, it seems that no one cares about how the objects implementing interfaces are born and how they die. This is the simplicity of Using Interfaces in Delphi and the goal it pursues in solving the problems of using interfaces. When an interface is required, there is always an object that will be generated for her. Once no interface is referenced, this object will die without complaints and will never drag down the system's one-byte resources. It is a bit sad that "Spring silkworms reach the dead silk, and the wax torch begins with tears.
Because the life and death of an interface object are directly related to the number of interfaces that reference this object, we will study when an interface reference will be added and when it will be reduced, it is the key to understanding the life and death of interface objects.
Now we can implement the simplest interface object class tintfobj, which only implements the three basic methods defined in the iunknown interface. Some friends will know that this class actually copied part of the tinterfacedobject class code in Delphi. However, we added some information output statements in the _ addref and _ release methods respectively, so that we can explore the Life and Death issues of interface objects. See the following program:
Program Programa;
Uses
Sysutils, dialogs;
Type
Tintfobj = Class (tobject, iunknown)
Protected
Frefcount: integer;
Function QueryInterface (const IID: tguid; out OBJ): hresult; stdcall;
Function _ addref: integer; stdcall;
Function _ release: integer; stdcall;
End;
Function tintfobj. QueryInterface (const IID: tguid; out OBJ): hresult; stdcall;
Const
E_nointerface = hresult ($80004002 );
Begin
If getinterface (IID, OBJ) then result: = 0 else result: = e_nointerface;
End;
Function tintfobj. _ addref: integer; stdcall;
Begin
INC (frefcount );
Showmessage (format ('crease reference count to % d. ', [frefcount]);
Result: = frefcount;
End;
Function tintfobj. _ release: integer; stdcall;
Begin
Dec (frefcount );
If frefcount <> 0 then
Showmessage (format ('crease reference count to % d. ', [frefcount])
Else begin
Destroy;
Showmessage ('crease reference count to 0, and destroy the object .');
End;
Result: = frefcount;
End;
VaR
Aobject: tintfobj;
Ainterface: iunknown;
Procedure intfobjlife;
Begin
Aobject: = tintfobj. Create;
Ainterface: = aobject; // Add a reference
Ainterface: = nil; // reduce one reference
End;
Begin
Intfobjlife;
End.
We need to use the single-step debugging function to study the relationship between the increase and decrease of interface reference count and interface life and death. Therefore, we recommend that you clear the optimization item on the complier page of the options Option to avoid the compiler optimizing the instructions we need.
When the program executes three lines of code to the intfobjlife subroutine, debug the code step by step. You will find that when a value is assigned to an interface type variable, it will increase or decrease the interface reference count.
Execution statement
Ainterface: = aobject;
The "reference count increase to 1." message appears, indicating that an interface reference is added.
Execute the statement
Ainterface: = nil;
"Reference count decrease to 0, and destroy the object." appears, indicating that the interface reference is reduced to zero and the interface object is deleted.
Therefore, we can conclude that when a value is assigned to a variable of the interface type, the reference count of the interface object will be increased; when the reference value of the interface type variable is cleared (NIL is assigned), the reference count of the interface object is reduced.
Let's take a look at the following code to deepen our understanding of this conclusion.
VaR
Aobject: tintfobj;
Interfacea, interfaceb: iunknown;
......
Aobject: = tintfobj. Create;
Interfacea: = aobject; // Add reference to 1
Interfacea: = interfacea; // Add reference to 2, but immediately decrease to 1
Interfaceb: = interfacea; // Add the reference to 2.
Interfacea: = nil; // reduce the number of references to 1
Interfaceb: = interfacea; // The reference is reduced to 0 and the object is released.
......
This conclusion is confirmed by assigning an interface object to a variable, an interface variable to an interface variable, and an nil value to an interface variable. Interestingly, when interfacea: = interfacea is executed, the reference of the interface object increases first and then decreases immediately. Why? Leave it for you to think!
Next, let's take a look at the following code:
Procedure intfobjlife;
VaR
Aobject: tintfobj;
Ainterface: iunknown;
Begin
Aobject: = tintfobj. Create;
Ainterface: = aobject;
End;
Different from the previous process, this process defines the variable as a local variable and does not assign the nil value to the interface variable. One-step debugging of this Code shows that the reference to the interface object is reduced to 0 and released before the program runs to the end statement of the subprogram. Why?
As we know, variables have scopes. The scope of global variables is any part of the program, and the scope of local variables is only within the corresponding subroutine. Once a variable leaves its scope, the variable itself does not exist, and its stored values are meaningless. Therefore, when the program is about to exit the subroutine, the local variable ainterface will not exist, and the reference value of the stored interface object will also become meaningless. Intelligent Delphi automatically reduces the reference count of interface objects to ensure that the program can correctly manage the memory space of interface objects in layer-by-layer calls and responses.
Therefore, we can come up with a new conclusion: When any interface variable is out of its scope, it will reduce the reference count of the relevant interface object.
It should be noted that the parameter variable of the subroutine is also a variable, and its scope is also within the scope of the subroutine. When you call a subroutine that contains an interface type parameter, the reference count of the relevant interface object will be increased due to parameter passing, and the return time of the subroutine will be reduced.
Similarly, if the reverse return value of a subroutine is of the interface type, the return value ranges from the return point of the main program to the end statement of the main program. This will also increase or decrease the reference count of the interface object.
This section summarizes the life and death of objects. We can come up with the following principles:
1. When you assign reference values of an interface object to elements such as global variables, local variables, parameter variables, and return values, the reference count of the interface object will be increased.
2. Before the API reference value stored in the variable is changed, the reference count of the associated object is reduced. Assigning nil values to variables is a special case of assigning values and modifying interface references. It only reduces the reference count of the original interface object and does not involve new interface references.
3. The global variables, local variables, parameter variables, return values, and other elements that store the interface reference values will automatically reduce the reference count of the interface objects when they exceed the scope.
4. When the reference count of an interface object is zero, the memory space of the interface object is automatically released. (In some middleware systems that adopt the object cache technology, such as MTS, this principle may not be followed)
You need to be reminded that once you hand over the created interface object to the interface, the object will be handed over to the interface. Just like marrying a baby daughter to a loyal man, you should trust him and trust him to take care of her. Since then, the contact with the object has to be through the interface, rather than directly dealing with the object. You know, it may be a big problem to bypass the son-in-law and directly intervene in the daughter-in-law. Believe it or not, let's look at the following code:
Program husbandofwife;
Type
Ihusband = interface
Function getsomething: string;
End;
Twife = Class (tinterfacedobject, ihusband)
Private
Fsomething: string;
Public
Constructor create (something: string );
Function getsomething: string;
End;
Constructor twife. Create (something: string );
Begin
Inherited create;
Fsomething: = something;
End;
Function twife. getsomething: string;
Begin
Result: = fsomething;
End;
Procedure husbanddoing (ahusband: ihusband );
Begin
End;
VaR
Thewife: twife;
Thehusb and: ihusb and;
Begin
Thewife: = twife. Create ('wan Guan Jia Cai ');
Thehusband: = thewife; // The thewife object is delegated to the general interface variable thehusb and
Thehusband: = nil; // clear interface reference, object disappears
Thewife. getsomething; // An error occurred while directly accessing the object!
Thewife: = twife. Create ('wan Guan Jia Cai ');
Husbanddoing (thewife); // The object is delegated to the parameter interface variable ahusband, and the returned object disappears.
Thewife. getsomething; // An error occurred while directly accessing the object!
End.
Take a closer look at the code from begin to end. I try to write the program interesting and easy to understand. I hope you can understand my program. It basically means that after a thewife object is generated, once the object is passed to an ihusband-type interface, directly manipulating the object using thewife may cause unimaginable problems! It's easy to get the money out, but it's hard to get it back.
Therefore, in the interface-based program design, please remember: once an interface object is created, always use the interface to manipulate the object!
Now, you should think that this topic is not scary! If so, I can write this section. I am also in a good mood because you can listen to me quietly. Thank you! Let's move forward in a pleasant mood.
Section 4 behind interface methods
As the saying goes, there must be a great woman behind every successful man. Similarly, each interface must have an amazing object. Today, we will learn how interfaces are mapped to objects to implement interface functions.
We all know that an interface value is actually a pointer. This pointer points to a method address table. Each item in the address table contains the address of the object method to be called. When you call a method through an interface value, you can find the method of the object through the method address mapped to the method address table, so as to call the method correctly.
In this way, there is nothing wrong with the method ing relationship between the interface and the object, but it ignores the problem of how to locate only the referenced object through the interface. Because an interface value not only represents a set of implemented methods, but also indicates that the method of this interface value is implemented by the object it references, it is not implemented by another object (although it is an object of the same class ).
Therefore, the call to the interface value first locates the object instance before calling the object method.
When we think about problems, we often make some habitual mistakes. Again, I am looking for a friend to borrow a car. I took the car key and went to the garage. When I got to the garage, I found that there were a lot of cars parked there, so I had to call to ask. Because I was eager to drive and forgot to ask what car it was. Similarly, many friends often ignore the ing of objects when discussing the ing between interfaces and objects.
In fact, the method table corresponding to the interface pointer does not directly store the object method address, but stores the address of a small segment of code that enables each interface method to jump to the object method. This short code first converts an interface pointer to an object pointer, and then jumps to the method address of the object and calls the method whose value is directed to the object.
In Delphi, the instance space of an interface object contains the method table pointer of all interfaces implemented by this object. If you think of the interface method table pointer in the object instance space as a data member of an object, the address value of the data is an interface reference value, that is, the interface pointer. In fact, the interface method table pointer is a data member of an interface object, and its offset in the object instance space is fixed. Therefore, Delphi only needs to subtract the fixed offset from the interface pointer to get the object pointer. The short code jump from an interface method to an object method implements this ing.
In short, the interface is the virtual method address TABLE statement is not completely correct, the interface implementation has more things to consider. However, this does not affect the use of Delphi to develop complex applications. This is only the mystery of implementing the interface mechanism in Delphi. It should be considered by the experts who created Delphi. Let's take a look at the interface method ing at the language level.
By default, all methods of the interface class are named with the same name as the methods of the implemented interface type, just like the virtual functions of the overloaded class. However, the method of the Interface Class and the method of the interface type are not overloaded, but corresponding. No one has said that the name of an interface class method must be the same as that of an interface type method, but this same name allows the Delphi compiler to automatically correspond to interfaces and class methods.
Suppose we define an imailbox interface as follows:
Imailbox = interface
Procedure putmail (amail: string );
Function getmail: string;
End;
We use the following tmailbox class to implement this interface:
Tmailbox = Class (tinterfacedobject, imailbox)
Procedure putmail (amail: string );
Function getmail: string;
End;
Because the tmailbox method name is exactly the same as the imailbox method name, the Delphi compiler automatically maps the interface method to the method of the relevant class object.
However, if we use the following tmailbox class to implement this interface:
Tmailbox = Class (tinterfacedobject, imailbox)
Procedure setmail (amail: string );
Function getmail: string;
End;
In this case, the compiler cannot find a definition that matches the imailbox putmail method in the tmailbox definition. The prompt "undeclared identifier: 'putmail'" is displayed '". In fact, we mean to use the setmail method of tmailbox to implement the putmail method of the imailbox interface, but they have different names. But the compiler does not know that it is not smart enough to understand natural language.
At this time, we need to use the interface method ing statement. Redefine tmailbox as follows:
Tmailbox = Class (tinterfacedobject, imailbox)
Procedure imailbox. putmail = setmail; // interface method ing Definition
Procedure setmail (amail: string );
Function getmail: string;
End;
Procedure imailbox. putmail = setmail; indicates that the putmail of imailbox is mapped to the setmail method of the class. In this way, the program can be compiled normally.
This again shows that the object class and interface are not inherited, and the object class method and interface method are not overloaded by virtual functions. Instead, the object class implements the interface, and the interface method is mapped to the object method.
You can think of a defined object class as designing a circuit board. If you design the input and output pins of a circuit board according to a general interface standard, the circuit board can be directly connected to the standard interface after it is designed. Otherwise, some Jumpers need to be welded to map the interface pins to the PCB pins.
The benefit of the interface concept is that the user who uses the interface may never know how the interface function is implemented, but this function is well applied. Interface users can implement interface functions in the most flexible way as possible, without worrying that outsiders will steal your technical secrets.
When an object class implements an interface in Delphi, it can delegate this implementation requirement to another object or interface. This gives the interface function provider a more flexible interface implementation method.
Let's take a look at the following program:
Program servicecenter;
Type
Iwasher = interface
Procedure washclothing;
End;
Iremover = interface
Procedure movehouse;
End;
Twasher = Class (tinterfacedobject, iwasher)
Procedure washclothing;
End;
Tremover = Class (tinterfacedobject, iremover)
Procedure movehouse;
End;
Tservicecenter = Class (tinterfacedobject, iwasher, iremover)
Private
Fwasher: twasher; // laundry target
Function findremover: iremover; // find a porter
Public
Constructor create;
Property ments Er: twasher read fwasher implements iwasher; // delegate implementation
Property remover: iremover read findremover implements iremover; // delegate implementation
End;
Procedure twasher. washclothing;
Begin
End;
Procedure tremover. movehouse;
Begin
End;
Constructor tservicecenter. Create;
Begin
Inherited;
Fwasher: = twasher. Create;
End;
Function tservicecenter. findremover: iremover;
Begin
Result: = tremover. Create;
End;
Begin
End.
In this program, we define the iwasher and iremover interfaces for the laundry and porters, as well as their implementation classes twasher and tremover. We have defined a service center class tservicecenter, which implements the iwasher and iremover interfaces. The service center can provide laundry and migration services, but it does not do laundry and migration by itself, but is entrusted to other laundry workers and porters. Two Definition statements:
Property ments Er: twasher read fwasher implements iwasher; // delegate implementation
Property remover: iremover read findremover implements iremover; // delegate implementation
Complete the delegation relationship of the service.
The service center object can be referenced by any interface that requires icycler and iremover, but it is only a middlemen. The specific work is done by twasher and tremover.
Tservicecenter creates a twasher object to implement the iwasher interface and provides the laundry service for the customer. Because the laundry service is a regular service, it is necessary to hire laundry workers when the service center is opened. However, when the iremover interface is required, it dynamically creates a tremover object and returns the interface. Because migration is not frequent, you only need to hire a porter to work as needed.
In short, the delegate implementation of interface functions is flexible and can be dynamic. By understanding these mysteries, you can write better interface-based programs. However, the program should be close to and reflect the actual things in life, which is called object-oriented.
The last two secrets sold in this section are for everyone to think about:
1. The fwasher object is created in the create constructor of tservicecenter. Is it necessary to release it in the corresponding destructor destroy?
2. Is it necessary to manage the memory of the tremover object temporarily created by tservicecenter?
I believe you can analyze and answer these two questions correctly.