Delphi:如何将代理接口实现到子对象? [英] Delphi: How delegate interface implementation to child object?

查看:219
本文介绍了Delphi:如何将代理接口实现到子对象?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有一个对象,它将一个特别复杂的接口的实现委托给一个子对象。这个正好是我认为是 TAggregatedObject 的工作。 对象保留对其 控制器 的弱引用,并且所有 QueryInterface 请求被传回给父进程。这维护了 IUnknown
始终是相同对象的规则。



所以,我的父(ie Controller)对象声明它实现 IStream 接口:

  type 
TRobot = class(TInterfacedObject,IStream)
private
function GetStream:IStream;
public
属性Stream:IStream读取GetStrem实现IStream;
结束




注意这是一个假设的例子。我选择了这个词 Robot
,因为它听起来很复杂,而
字只有5个字母,这是
短。我还选择了 IStream ,因为
它的短。我将使用
IPersistFile IPersistFileInit
,但它们更长,使
示例代码更难实现。在其他
字:这是一个假设的例子。


现在我有我的孩子对象将实现 IStream

 键入
TRobotStream = class(TAggregatedObject,IStream)
public
...
end;

剩下的所有内容,这是我的问题开始的地方:创建 RobotStream 当被要求时:

 功能TRobot.GetStream:IStream; 
begin
结果:= TRobotStream.Create(Self)as IStream;
结束

此代码无法编译,错误操作符不适用于此操作数键入



这是因为delphi正在尝试执行 作为IStream 对不实现 IUnknown 的对象:

  TAggregatedObject = class 
...
{IUnknown}
function QueryInterface(const IID:TGUID; out Obj):HResult;标准
function _AddRef:Integer;标准
函数_Release:整数;标准
...

IUnknown 方法可能在那里,但是该对象不会广告,它支持 IUnknown 。没有一个 IUnknown 接口,Delphi不能调用 QueryInterface 执行转换。



所以我更改我的 TRobotStream 类来宣传它实现缺少的接口(它所做的;它从它的祖先继承它):

  type 
TRobotStream = class(TAggregatedObject,IUnknown,IStream)
...

现在,它编译,但在运行时在行上崩溃:

  Result:= TRobotStream.Create(Self)as IStream; 

现在我可以看到发生什么,但是我无法解释<强>为什么。 Delphi在子对象的构造函数的出路上调用 IntfClear ,在我的父代 Robot 对象上。 >

我不知道正确的方法来防止这种情况。我可以尝试强制演员:

  Result:= TRobotStream.Create(Self as IUnknown)as IStream; 

,希望能保持参考。事实证明它确实保留了引用 - 在构造函数的出路上没有崩溃。


注意:这对我来说很混乱因为我传递一个对象,其中
一个界面是预期的。我会
假设编译器是隐式的
执行类型转换,即:



Result:= TRobotStream.Create自我 为IUnknown );



为了满足电话。
事实上,语法检查器没有
抱怨让我假设所有的
正确。







但是崩溃还没有结束。我已经改为:

  Result:= TRobotStream.Create(Self as IUnknown)as IStream; 

代码确实从的构造函数返回TRobotStream 而不会破坏我的父对象,但现在我得到一个堆栈溢出。



原因是 TAggregatedObject 将所有 QueryInterface (即类型转换)返回到父对象。在我的情况下,我将一个 TRobotStream 转换为 IStream



当我在 IStream 的末尾请求 TRobotStream

 结果:= TRobotStream.Create(Self as IUnknown)as IStream; 

它转过头来询问控制器 IStream 接口,触发调用:

 结果:= TRobotStream.Create IUnknown)作为IStream; 
结果:= TRobotStream.Create(Self as IUnknown)as IStream;

其中转过来并呼叫:

  Result:= TRobotStream.Create(Self as IUnknown)as IStream; 
结果:= TRobotStream.Create(Self as IUnknown)as IStream;
结果:= TRobotStream.Create(Self as IUnknown)as IStream;

Boom! 堆栈溢出。 p>




盲目地,我尝试删除最终的演员到 IStream ,让德尔福尝试暗示将对象投射到一个界面(我刚刚看到上面的内容不正确):

 结果:= TRobotStream创建(Self as IUnknown); 

现在没有崩溃;我不太了解这个。我已经构建了一个对象,一个支持多个接口的对象。 Delphi现在怎么知道这个界面呢?它是否执行适当的引用计数?我看到上面没有。是否有一个微妙的错误等待客户崩溃?



所以我剩下四种可能的方法来调用我的一行。哪一个是有效的?


  1. 结果:= TRobotStream.Create(Self);

  2. 结果:= TRobotStream.Create(Self as IUnknown);

  3. = TRobotStream.Create(Self)as IStream;

  4. Result:= TRobotStream.Create(Self as IUnknown)as IStream; code>



真实问题



几个微妙的错误,难以理解编译器的复杂性。这使我相信我已经完成了一切都错了。如果需要,请忽略我所说的一切,并帮助我回答以下问题:


将接口实现委托给子对象的正确方法是什么? ?


也许我应该使用 TContainedObject 而不是 TAggregatedObject 。也许这两个工作在一起,父母应该是 TAggregatedObject ,孩子是 TContainedObject 。也许这是另一回事。


注意:主要部分中的所有内容我的帖子可以忽略不计。只有
表明我已经考虑过了。
有人会认为
包含我已经尝试过,我有
中毒了可能的答案;而
比回答我的问题,人民币
可能会关注我的失败问题。



真正的目标是将界面
实现委托给一个孩子的对象。这个
问题包含我详细的尝试
在解决
TAggregatedObject 的问题。你甚至没有
看到我的其他两个解决方案模式。
其中一个受到循环
引用计数,并且中断
IUnknown 等价规则。


罗伯·肯尼迪可能会记得;并要求
我提出一个问题,要求一个
的解决方案的问题,而不是一个
解决我的一个
解决方案中的问题。


编辑:格式化



编辑2: / strong>没有机器人控制器这样的东西。那么 - 有 - 我一直在使用Funuc RJ2控制器。但是不在这个例子中!



编辑3 *

 code> TRobotStream = class(TAggregatedObject,IStream)
public
{IStream}
函数Seek(dlibMove:Largeint; dwOrigin:Longint;
out libNewPosition:Largeint):人力资源标准
函数SetSize(libNewSize:Largeint):HResult;标准
function CopyTo(stm:IStream; cb:Largeint; out cbRead:Largeint; out cbWritten:Largeint):HResult;标准
function Commit(grfCommitFlags:Longint):HResult;标准
函数Revert:HResult;标准
function LockRegion(libOffset:Largeint; cb:Largeint; dwLockType:Longint):HResult;标准
function UnlockRegion(libOffset:Largeint; cb:Largeint; dwLockType:Longint):HResult;标准
函数Stat(out statstg:TStatStg; grfStatFlag:Longint):HResult;标准
function Clone(out stm:IStream):HResult;标准

函数读(pv:指针; cb:Longint; pcbRead:PLongint):HResult;标准
函数写(pv:指针; cb:Longint; pcbWritten:PLongint):HResult;标准
结束

TRobot = class(TInterfacedObject,IStream)
private
FStream:TRobotStream;
函数GetStream:IStream;
public
析构函数Destroy;覆盖
属性Stream:IStream读取GetStream实现IStream;
结束

var
Form1:TForm1;

执行

{$ R * .DFM}

程序TForm1.Button1Click(发件人:TObject);
var
rs:IStream;
begin
rs:= TRobot.Create;
LoadRobotFromDatabase(rs); //虚拟方法,只是为了演示我们使用流
rs:= nil;
结束

程序TForm1.LoadRobotFromDatabase(rs:IStream);
begin
rs.Revert; //虚方法调用,只是为了证明我们可以调用它
end;

析构函数TRobot.Destroy;
begin
FStream.Free;
继承;
结束

功能TRobot.GetStream:IStream;
begin
如果FStream = nil then
FStream:= TRobotStream.Create(Self);
result:= FStream;
结束

这里的问题是父母 TRobot 对象在调用期间被销毁:

  FStream:= TRobotStream.Create(Self); 


解决方案

您必须为创建的子项添加一个字段实例对象:

  type 
TRobot = class(TInterfacedObject,IStream)
private
FStream: TRobotStream;
函数GetStream:IStream;
public
属性Stream:IStream读取GetStream实现IStream;
结束

析构函数TRobot.Destroy;
begin
FStream.Free;
继承;
结束

功能TRobot.GetStream:IStream;
begin
如果FStream = nil then
FStream:= TRobotStream.Create(Self);
result:= FStream;
结束

更新
TRobotStream应该已经从TAggregatedObject派生猜到了声明应该是:

  type 
TRobotStream = class(TAggregatedObject,IStream)
...
结束

没有必要提到IUnknown。



在TRobot.GetStream中,行 result:= FStream 将一个隐含的 FStream作为IStream 所以写出来也不是必需的。



FStream必须被声明为TRobotStream而不是IStream,所以当它被TRobot实例被销毁。注意:TAggregatedObject没有引用计数,所以容器必须照顾其使用寿命。



更新(Delphi 5代码):

  unit Unit1; 

接口

使用
Windows,消息,SysUtils,类,图形,控件,窗体,
对话框,StdCtrls,activex,comobj;

type
TForm1 = class(TForm)
Button1:TButton;
Edit1:TEdit;
procedure Button1Click(Sender:TObject);
private
procedure LoadRobotFromDatabase(rs:IStream);
public
end;

type
TRobotStream = class(TAggregatedObject,IStream)
public
{IStream}
函数Seek(dlibMove:Largeint; dwOrigin:Longint;
out libNewPosition:Largeint):HResult;标准
函数SetSize(libNewSize:Largeint):HResult;标准
function CopyTo(stm:IStream; cb:Largeint; out cbRead:Largeint; out cbWritten:Largeint):HResult;标准
function Commit(grfCommitFlags:Longint):HResult;标准
函数Revert:HResult;标准
function LockRegion(libOffset:Largeint; cb:Largeint; dwLockType:Longint):HResult;标准
function UnlockRegion(libOffset:Largeint; cb:Largeint; dwLockType:Longint):HResult;标准
函数Stat(out statstg:TStatStg; grfStatFlag:Longint):HResult;标准
function Clone(out stm:IStream):HResult;标准
函数读(pv:指针; cb:Longint; pcbRead:PLongint):HResult;标准
函数写(pv:指针; cb:Longint; pcbWritten:PLongint):HResult;标准
结束

type
TRobot = class(TInterfacedObject,IStream)
private
FStream:TRobotStream;
函数GetStream:IStream;
public
析构函数Destroy;覆盖
属性Stream:IStream读取GetStream实现IStream;
结束

var
Form1:TForm1;

实现

{$ R * .dfm}

程序TForm1.Button1Click(发件人:TObject);
var
rs:IStream;
begin
rs:= TRobot.Create;
LoadRobotFromDatabase(rs); //虚拟方法,只是为了演示我们使用流
rs:= nil;
结束

程序TForm1.LoadRobotFromDatabase(rs:IStream);
begin
rs.Revert; //虚方法调用,只是为了证明我们可以调用它
end;

函数TRobotStream.Clone(out stm:IStream):HResult;
begin
end;

函数TRobotStream.Commit(grfCommitFlags:Integer):HResult;
begin
end;

函数TRobotStream.CopyTo(stm:IStream; cb:Largeint; out cbRead,cbWritten:Largeint):HResult;
begin
end;

函数TRobotStream.LockRegion(libOffset,cb:Largeint; dwLockType:Integer):HResult;
begin
end;

函数TRobotStream.Read(pv:Pointer; cb:Integer; pcbRead:PLongint):HResult;
begin
end;

函数TRobotStream.Revert:HResult;
begin
end;

函数TRobotStream.Seek(dlibMove:Largeint; dwOrigin:Integer;
out libNewPosition:Largeint):HResult;
begin
end;

函数TRobotStream.SetSize(libNewSize:Largeint):HResult;
begin
end;

函数TRobotStream.Stat(out statstg:TStatStg; grfStatFlag:Integer):HResult;
begin
end;

函数TRobotStream.UnlockRegion(libOffset,cb:Largeint; dwLockType:Integer):HResult;
begin
end;

函数TRobotStream.Write(pv:Pointer; cb:Integer; pcbWritten:PLongint):HResult;
begin
end;

析构函数TRobot.Destroy;
begin
FStream.Free;
继承;
结束

功能TRobot.GetStream:IStream;
begin
如果FStream = nil then
FStream:= TRobotStream.Create(Self);
result:= FStream;
结束

结束。
在此输入代码


i have an object which delegates implementation of a particularly complex interface to a child object. This is exactly i think is the job of TAggregatedObject. The "child" object maintains a weak reference to its "controller", and all QueryInterface requests are passed back to the parent. This maintains the rule that IUnknown is always the same object.

So, my parent (i.e. "Controller") object declares that it implements the IStream interface:

type
   TRobot = class(TInterfacedObject, IStream)
   private
      function GetStream: IStream;
   public
      property Stream: IStream read GetStrem implements IStream;
   end;

Note: This is a hypothetical example. i chose the word Robot because it sounds complicated, and and word is only 5 letters long - it's short. i also chose IStream because its short. i was going to use IPersistFile or IPersistFileInit, but they're longer, and make the example code harder to real. In other words: It's a hypothetical example.

Now i have my child object that will implement IStream:

type
   TRobotStream = class(TAggregatedObject, IStream)
   public
      ...
   end;

All that's left, and this is where my problem starts: creating the RobotStream when it's asked for:

function TRobot.GetStream: IStream;
begin
    Result := TRobotStream.Create(Self) as IStream;
end;

This code fails to compile, with the error Operator not applicable to this operand type..

This is because delphi is trying to perform the as IStream on an object that doesn't implement IUnknown:

TAggregatedObject = class
 ...
   { IUnknown }
   function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
   function _AddRef: Integer; stdcall;
   function _Release: Integer; stdcall;
 ...

The IUnknown methods may be there, but the object doesn't advertise that it supports IUnknown. Without an IUnknown interface, Delphi can't call QueryInterface to perform the cast.

So i change my TRobotStream class to advertise that it implements the missing interface (which it does; it inherits it from its ancestor):

type
   TRobotStream = class(TAggregatedObject, IUnknown, IStream)
   ...

And now it compiles, but crashes at runtime on the line:

Result := TRobotStream.Create(Self) as IStream;

Now i can see what's happening, but i can't explain why. Delphi is calling IntfClear, on my parent Robot object, on the way out of the child object's constructor.

i don't know the proper way to prevent this. i could try forcing the cast:

Result := TRobotStream.Create(Self as IUnknown) as IStream;

and hope that keeps a reference. Turns out that it does keep the reference - no crash on the way out of the constructor.

Note: This is confusing to me. Since i am passing an object where an interface is expected. i would assume that the compiler is implicitly preforming a typecast, i.e.:

Result := TRobotStream.Create(Self as IUnknown);

in order to satisfy the call. The fact that the syntax checker didn't complain let me to assume all was correct.


But the crashes aren't over. i've changed the line to:

Result := TRobotStream.Create(Self as IUnknown) as IStream;

And the code does indeed return from the constructor of TRobotStream without destroying my parent object, but now i get a stack overflow.

The reason is that TAggregatedObject defers all QueryInterface (i.e. type casts) back to the parent object. In my case i am casting a TRobotStream to an IStream.

When i ask the TRobotStream for its IStream at the end of:

Result := TRobotStream.Create(Self as IUnknown) as IStream;

It turns around and asks its controller for the IStream interface, which triggers a call to:

Result := TRobotStream.Create(Self as IUnknown) as IStream;
   Result := TRobotStream.Create(Self as IUnknown) as IStream;

which turns around and calls:

Result := TRobotStream.Create(Self as IUnknown) as IStream;
   Result := TRobotStream.Create(Self as IUnknown) as IStream;
      Result := TRobotStream.Create(Self as IUnknown) as IStream;

Boom! Stack overflow.


Blindly, i try removing the final cast to IStream, let Delphi try to implicitely cast the object to an interface (which i just saw above doesn't work right):

Result := TRobotStream.Create(Self as IUnknown);

And now there is no crash; which i don't understand this very much. i've constructed an object, an object which supports multiple interfaces. How is it now that Delphi knows to cast the interface? Is it performing the proper reference counting? i saw above that it doesn't. Is there a subtle bug waiting to crash for the customer?

So i'm left with four possible ways to call my one line. Which one of them is valid?

  1. Result := TRobotStream.Create(Self);
  2. Result := TRobotStream.Create(Self as IUnknown);
  3. Result := TRobotStream.Create(Self) as IStream;
  4. Result := TRobotStream.Create(Self as IUnknown) as IStream;

The Real Question

i hit quite a few subtle bugs, and difficult to understand intricacies of the compiler. This leads me to believe that i have done everything completely wrong. If needed, ignore everything i said, and help me answer the question:

What is the proper way to delegate interface implementation to a child object?

Maybe i should be using TContainedObject instead of TAggregatedObject. Maybe the two work in tandem, where the parent should be TAggregatedObject and the child is TContainedObject. Maybe it's the other way around. Maybe neither apply in this case.

Note: Everything in the main part of my post can be ignored. It was just to show that i have thought about it. There are those who would argue that by including what i have tried, i have poisoned the possible answers; rather than answering my question, people might focus on my failed question.

The real goal is to delegate interface implementation to a child object. This question contains my detailed attempts at solving the problem with TAggregatedObject. You don't even see my other two solution patterns. One of which suffers from circular refernce counts, and the breaks the IUnknown equivalence rule.

Rob Kennedy might remember; and asked me to make a question that asks for a solution to the problem, rather than a solution to a problem in one of my solutions.

Edit: grammerified

Edit 2: No such thing as a robot controller. Well, there is - i worked with Funuc RJ2 controllers all the time. But not in this example!

Edit 3*

  TRobotStream = class(TAggregatedObject, IStream)
    public
        { IStream }
     function Seek(dlibMove: Largeint; dwOrigin: Longint;
        out libNewPosition: Largeint): HResult; stdcall;
     function SetSize(libNewSize: Largeint): HResult; stdcall;
     function CopyTo(stm: IStream; cb: Largeint; out cbRead: Largeint; out cbWritten: Largeint): HResult; stdcall;
     function Commit(grfCommitFlags: Longint): HResult; stdcall;
     function Revert: HResult; stdcall;
     function LockRegion(libOffset: Largeint; cb: Largeint; dwLockType: Longint): HResult; stdcall;
     function UnlockRegion(libOffset: Largeint; cb: Largeint; dwLockType: Longint): HResult; stdcall;
     function Stat(out statstg: TStatStg; grfStatFlag: Longint): HResult; stdcall;
     function Clone(out stm: IStream): HResult; stdcall;

     function Read(pv: Pointer; cb: Longint; pcbRead: PLongint): HResult; stdcall;
     function Write(pv: Pointer; cb: Longint; pcbWritten: PLongint): HResult; stdcall;
  end;

  TRobot = class(TInterfacedObject, IStream)
  private
      FStream: TRobotStream;
      function GetStream: IStream;
  public
     destructor Destroy; override;
      property Stream: IStream read GetStream implements IStream;
  end;

var
  Form1: TForm1;

implementation

{$R *.DFM}

procedure TForm1.Button1Click(Sender: TObject);
var
    rs: IStream;
begin
    rs := TRobot.Create;
    LoadRobotFromDatabase(rs); //dummy method, just to demonstrate we use the stream
    rs := nil;
end;

procedure TForm1.LoadRobotFromDatabase(rs: IStream);
begin
    rs.Revert; //dummy method call, just to prove we can call it
end;

destructor TRobot.Destroy;
begin
  FStream.Free;
  inherited;
end;

function TRobot.GetStream: IStream;
begin
  if FStream = nil then
     FStream := TRobotStream.Create(Self);
  result := FStream;
end;

Problem here is that the "parent" TRobot object is destroyed during the call to:

FStream := TRobotStream.Create(Self);

解决方案

You have to add a field instance for the created child object:

type
  TRobot = class(TInterfacedObject, IStream)
  private
     FStream: TRobotStream;
     function GetStream: IStream;
  public
     property Stream: IStream read GetStream implements IStream;
  end;

destructor TRobot.Destroy;
begin
  FStream.Free; 
  inherited; 
end;

function TRobot.GetStream: IStream;
begin
  if FStream = nil then 
    FStream := TRobotStream.Create(Self);
  result := FStream;
end;

Update TRobotStream should be derived from TAggregatedObject as you already guessed. The declaration should be:

type
  TRobotStream = class(TAggregatedObject, IStream)
   ...
  end;

It is not necessary to mention IUnknown.

In TRobot.GetStream the line result := FStream does an implicite FStream as IStream so writing this out isn't necessary either.

FStream has to be declared as TRobotStream and not as IStream so it can be destroyed when the TRobot instance is destroyed. Note: TAggregatedObject has no reference counting so the container has to take care of its lifetime.

Update (Delphi 5 code):

unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, activex, comobj;

type
  TForm1 = class(TForm)
    Button1: TButton;
    Edit1: TEdit;
    procedure Button1Click(Sender: TObject);
  private
    procedure LoadRobotFromDatabase(rs: IStream);
  public
  end;

type
  TRobotStream = class(TAggregatedObject, IStream)
  public
    { IStream }
    function Seek(dlibMove: Largeint; dwOrigin: Longint;
       out libNewPosition: Largeint): HResult; stdcall;
    function SetSize(libNewSize: Largeint): HResult; stdcall;
    function CopyTo(stm: IStream; cb: Largeint; out cbRead: Largeint; out cbWritten: Largeint): HResult; stdcall;
    function Commit(grfCommitFlags: Longint): HResult; stdcall;
    function Revert: HResult; stdcall;
    function LockRegion(libOffset: Largeint; cb: Largeint; dwLockType: Longint): HResult; stdcall;
    function UnlockRegion(libOffset: Largeint; cb: Largeint; dwLockType: Longint): HResult; stdcall;
    function Stat(out statstg: TStatStg; grfStatFlag: Longint): HResult; stdcall;
    function Clone(out stm: IStream): HResult; stdcall;
    function Read(pv: Pointer; cb: Longint; pcbRead: PLongint): HResult; stdcall;
    function Write(pv: Pointer; cb: Longint; pcbWritten: PLongint): HResult; stdcall;
  end;

type
  TRobot = class(TInterfacedObject, IStream)
  private
    FStream: TRobotStream;
    function GetStream: IStream;
  public
    destructor Destroy; override;
    property Stream: IStream read GetStream implements IStream;
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.Button1Click(Sender: TObject);
var
  rs: IStream;
begin
  rs := TRobot.Create;
  LoadRobotFromDatabase(rs); //dummy method, just to demonstrate we use the stream
  rs := nil;
end;

procedure TForm1.LoadRobotFromDatabase(rs: IStream);
begin
  rs.Revert; //dummy method call, just to prove we can call it
end;

function TRobotStream.Clone(out stm: IStream): HResult;
begin
end;

function TRobotStream.Commit(grfCommitFlags: Integer): HResult;
begin
end;

function TRobotStream.CopyTo(stm: IStream; cb: Largeint; out cbRead, cbWritten: Largeint): HResult;
begin
end;

function TRobotStream.LockRegion(libOffset, cb: Largeint; dwLockType: Integer): HResult;
begin
end;

function TRobotStream.Read(pv: Pointer; cb: Integer; pcbRead: PLongint): HResult;
begin
end;

function TRobotStream.Revert: HResult;
begin
end;

function TRobotStream.Seek(dlibMove: Largeint; dwOrigin: Integer;
  out libNewPosition: Largeint): HResult;
begin
end;

function TRobotStream.SetSize(libNewSize: Largeint): HResult;
begin
end;

function TRobotStream.Stat(out statstg: TStatStg; grfStatFlag: Integer): HResult;
begin
end;

function TRobotStream.UnlockRegion(libOffset, cb: Largeint; dwLockType: Integer): HResult;
begin
end;

function TRobotStream.Write(pv: Pointer; cb: Integer; pcbWritten: PLongint): HResult;
begin
end;

destructor TRobot.Destroy;
begin
  FStream.Free;
  inherited;
end;

function TRobot.GetStream: IStream;
begin
  if FStream = nil then
     FStream := TRobotStream.Create(Self);
  result := FStream;
end;

end.
enter code here

这篇关于Delphi:如何将代理接口实现到子对象?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆