重新抛出错误的堆栈跟踪

发布于 2024-10-12 21:41:02 字数 746 浏览 2 评论 0原文

我用“throw;”重新抛出异常,但堆栈跟踪不正确:

static void Main(string[] args) {
    try {
        try {
            throw new Exception("Test"); //Line 12
        }
        catch (Exception ex) {
            throw; //Line 15
        }
    }
    catch (Exception ex) {
        System.Diagnostics.Debug.Write(ex.ToString());
    }
    Console.ReadKey();
}

正确的堆栈跟踪应该是:

System.Exception:测试
   在 Program.cs 中的 ConsoleApplication1.Program.Main(String[] args):第 12 行

但我得到:

System.Exception:测试
   在 Program.cs 中的 ConsoleApplication1.Program.Main(String[] args):第 15 行

但第 15 行是“throw;”的位置。我已经用 .NET 3.5 对此进行了测试。

I rethrow an exception with "throw;", but the stacktrace is incorrect:

static void Main(string[] args) {
    try {
        try {
            throw new Exception("Test"); //Line 12
        }
        catch (Exception ex) {
            throw; //Line 15
        }
    }
    catch (Exception ex) {
        System.Diagnostics.Debug.Write(ex.ToString());
    }
    Console.ReadKey();
}

The right stacktrace should be:

System.Exception: Test
   at ConsoleApplication1.Program.Main(String[] args) in Program.cs:Line 12

But I get:

System.Exception: Test
   at ConsoleApplication1.Program.Main(String[] args) in Program.cs:Line 15

But line 15 is the position of the "throw;". I have tested this with .NET 3.5.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(12

隔岸观火 2024-10-19 21:41:03

在同一个方法中抛出两次可能是一种特殊情况 - 我无法创建堆栈跟踪,其中同一方法中的不同行彼此跟随。正如该词所说,“堆栈跟踪”向您显示异常遍历的堆栈帧。而且每个方法调用只有一个堆栈帧!

如果您从另一个方法抛出,throw; 将不会按预期删除 Foo() 的条目:

  static void Main(string[] args)
  {
     try
     {
        Rethrower();
     }
     catch (Exception ex)
     {
        Console.Write(ex.ToString());
     }
     Console.ReadKey();
  }

  static void Rethrower()
  {
     try
     {
        Foo();
     }
     catch (Exception ex)
     {
        throw;
     }

  }

  static void Foo()
  {
     throw new Exception("Test"); 
  }

如果您修改 Rethrower() 并将 throw; 替换为 throw ex;,堆栈跟踪中的 Foo() 条目就会消失。同样,这是预期的行为。

Throwing twice in the same method is probably a special case - I've not been able to create a stack trace where different lines in the same method follow each other. As the word says, a "stack trace" shows you the stack frames that an exception traversed. And there is only one stack frame per method call!

If you throw from another method, throw; will not remove the entry for Foo(), as expected:

  static void Main(string[] args)
  {
     try
     {
        Rethrower();
     }
     catch (Exception ex)
     {
        Console.Write(ex.ToString());
     }
     Console.ReadKey();
  }

  static void Rethrower()
  {
     try
     {
        Foo();
     }
     catch (Exception ex)
     {
        throw;
     }

  }

  static void Foo()
  {
     throw new Exception("Test"); 
  }

If you modify Rethrower() and replace throw; by throw ex;, the Foo() entry in the stack trace disappears. Again, that's the expected behavior.

浮生未歇 2024-10-19 21:41:03

这也算是意料之中的事情了。
修改堆栈跟踪是常见情况,如果您指定 throw ex;,FxCop 将通知您堆栈已修改。如果您进行 throw;,不会生成警告,但跟踪仍然会被修改。
所以不幸的是,现在最好不要抓住前任或将其作为内心的人扔掉。
我认为它应该被视为Windows 影响或类似的-编辑
Jeff Richter 在他的“CLR via C#” 中更详细地描述了这种情况:

下面的代码会抛出相同的错误
它捕获的异常对象和
导致 CLR 重置其启动
例外点:

private void SomeMethod() {
  尝试 { ... }
  捕获(异常e){
    ...
    扔 e; // CLR 认为这是异常产生的地方。
    // FxCop 将此报告为错误
  }
}

相反,如果你重新抛出一个
使用 throw 抛出异常对象
关键字本身,CLR 不会
重置堆栈的起点。这
以下代码重新抛出相同的错误
它捕获的异常对象,
导致 CLR 不重置其
异常的起始点:

private void SomeMethod() {
  尝试 { ... }
  捕获(异常e){
    ...
    扔; // 这对 CLR 认为异常的位置没有影响
    //起源。 FxCop 不会将此报告为错误
  }
}

事实上,唯一的区别是
这两个代码片段是什么
CLR认为是原来的位置
抛出异常的地方。
不幸的是,当你扔或
重新抛出异常,Windows 会这样做
重置堆栈的起点。
所以
如果异常未处理,
报告的堆栈位置
Windows 错误报告是
最后一次投掷的位置或
重新抛出,即使 CLR 知道
原来的堆栈位置
抛出异常。这是
不幸的是,因为它使得调试
失败的应用程序
场要困难得多。一些
开发人员发现了这一点
无法忍受他们选择了
不同的方式来实现他们的代码
以确保堆栈跟踪真实
反映了一个位置
最初抛出异常:

private void SomeMethod() {
  布尔值 trySucceeds = false;
  尝试 {
    ...
    尝试成功 = true;
  }
  最后 {
    if (!trySucceeds) { /* catch 代码放在这里 */ }
  }
}

It's something that can be considered as expected.
Modifying stack trace is usual case if you specify throw ex;, FxCop will than notify you that stack is modified. In case you make throw;, no warning is generated, but still, the trace will be modified.
So unfortunately for now it's the best not to catch the ex or throw it as an inner one.
I think it should be considered as a Windows impact or smth like that - edited.
Jeff Richter describes this situation in more detail in his "CLR via C#":

The following code throws the same
exception object that it caught and
causes the CLR to reset its starting
point for the exception:

private void SomeMethod() {
  try { ... }
  catch (Exception e) {
    ...
    throw e; // CLR thinks this is where exception originated.
    // FxCop reports this as an error
  }
}

In contrast, if you re-throw an
exception object by using the throw
keyword by itself, the CLR doesn’t
reset the stack’s starting point. The
following code re-throws the same
exception object that it caught,
causing the CLR to not reset its
starting point for the exception:

private void SomeMethod() {
  try { ... }
  catch (Exception e) {
    ...
    throw; // This has no effect on where the CLR thinks the exception
    // originated. FxCop does NOT report this as an error
  }
}

In fact, the only difference between
these two code fragments is what the
CLR thinks is the original location
where the exception was thrown.
Unfortunately, when you throw or
rethrow an exception, Windows does
reset the stack’s starting point.
So
if the exception becomes unhandled,
the stack location that gets reported
to Windows Error Reporting is the
location of the last throw or
re-throw, even though the CLR knows
the stack location where the original
exception was thrown. This is
unfortunate because it makes debugging
applications that have failed in the
field much more difficult. Some
developers have found this so
intolerable that they have chosen a
different way to implement their code
to ensure that the stack trace truly
reflects the location where an
exception was originally thrown:

private void SomeMethod() {
  Boolean trySucceeds = false;
  try {
    ...
    trySucceeds = true;
  }
  finally {
    if (!trySucceeds) { /* catch code goes in here */ }
  }
}
老子叫无熙 2024-10-19 21:41:03

这是 Windows 版本的 CLR 中众所周知的限制。它使用 Windows 内置的异常处理 (SEH) 支持。问题是,它是基于堆栈帧的,并且一种方法只有一个堆栈帧。您可以通过将内部 try/catch 块移动到另一个辅助方法中来轻松解决该问题,从而创建另一个堆栈帧。此限制的另一个后果是 JIT 编译器不会内联任何包含 try 语句的方法。

This is a well known limitation in the Windows version of the CLR. It uses Windows' built-in support for exception handling (SEH). Problem is, it is stack frame based and a method has only one stack frame. You can easily solve the problem by moving the inner try/catch block into another helper method, thus creating another stack frame. Another consequence of this limitation is that the JIT compiler won't inline any method that contains a try statement.

国际总奸 2024-10-19 21:41:03

如何保留真实的堆栈跟踪?

您抛出一个新异常,并将原始异常作为内部异常包含在内。

但这很丑......更长......让你选择正确的异常抛出......

您对丑陋的看法是错误的,但对其他两点的看法是正确的。经验法则是:除非你打算用它做一些事情,比如包装它、修改它、吞下它或记录它,否则不要捕获它。如果您决定抓住然后再次抛出,请确保您正在用它做一些事情,否则就让它冒泡。

您可能还想简单地放置一个 catch,以便可以在 catch 中设置断点,但 Visual Studio 调试器有足够的选项使这种做法变得不必要,请尝试使用第一次机会异常或条件断点。

How can I preserve the REAL stacktrace?

You throw a new exception, and include the original exception as the inner exception.

but that's Ugly... Longer... Makes you choice the rigth exception to throw....

You are wrong about the ugly but right about the other two points. The rule of thumb is: don't catch unless you are going to do something with it, like wrap it, modify it, swallow it, or log it. If you decide to catch and then throw again, make sure you are doing something with it, otherwise just let it bubble up.

You may also be tempted to put a catch simply so you can breakpoint within the catch, but the Visual Studio debugger has enough options to make that practice unnecessary, try using first chance exceptions or conditional breakpoints instead.

2024-10-19 21:41:03

编辑/替换

行为实际上有所不同,但很微妙。至于为什么行为不同,我需要听从 CLR 专家的意见。

编辑:AlexD的回答< /a> 似乎表明这是设计使然。

在捕获异常的同一个方法中抛出异常会使情况有点混乱,所以让我们从另一个方法抛出异常:

class Program
{
    static void Main(string[] args)
    {
        try
        {
            Throw();
        }
        catch (Exception ex)
        {
            throw ex;
        }
    }

    public static void Throw()
    {
        int a = 0;
        int b = 10 / a;
    }
}

If throw; 时,调用堆栈为(用代码替换行号):

at Throw():line (int b = 10 / a;)
at Main():line (throw;) // This has been modified

如果使用 throw ex;,则调用堆栈为:

at Main():line (throw ex;)

如果未捕获异常,则调用堆栈为:

at Throw():line (int b = 10 / a;)
at Main():line (Throw())

在 .NET 中测试4 / VS 2010

Edit/Replace

The behavior is actually different, but subtilely so. As for why the behavior if different, I'll need to defer to a CLR expert.

EDIT: AlexD's answer seems to indicate that this is by design.

Throwing the exception in the same method that catches it confuses the situation a little, so let's throw an exception from another method:

class Program
{
    static void Main(string[] args)
    {
        try
        {
            Throw();
        }
        catch (Exception ex)
        {
            throw ex;
        }
    }

    public static void Throw()
    {
        int a = 0;
        int b = 10 / a;
    }
}

If throw; is used, the callstack is (line numbers replaced with code):

at Throw():line (int b = 10 / a;)
at Main():line (throw;) // This has been modified

If throw ex; is used, the callstack is:

at Main():line (throw ex;)

If exception is not caught, the callstack is:

at Throw():line (int b = 10 / a;)
at Main():line (Throw())

Tested in .NET 4 / VS 2010

智商已欠费 2024-10-19 21:41:03

这里有一个重复的问题

据我了解 - 扔;被编译成 'rethrow' MSIL 指令,它修改了最后一帧堆栈跟踪。

我希望它保留原始堆栈跟踪并在重新抛出的位置添加行,但显然每个方法调用只能有一个堆栈帧

结论:避免使用 throw;并在重新抛出时将您的异常包装在一个新的异常中 - 这并不难看,这是最佳实践。

There is a duplicate question here.

As I understand it - throw; is compiled into 'rethrow' MSIL instruction and it modifies the last frame of the stack-trace.

I would expect it to keep the original stack-trace and add the line where it has been re-thrown, but apparently there can only be one stack frame per method call.

Conclusion: avoid using throw; and wrap your exception in a new one on re-throwing - it's not ugly, it's best practice.

吃→可爱长大的 2024-10-19 21:41:03

保留堆栈跟踪

ExceptionDispatchInfo.Capture(ex);

您可以使用以下代码示例

    static void CallAndThrow()
    {
        throw new ApplicationException("Test app ex", new Exception("Test inner ex"));
    }

    static void Main(string[] args)
    {
        try
        {
            try
            {
                try
                {
                    CallAndThrow();
                }
                catch (Exception ex)
                {
                    var dispatchException = ExceptionDispatchInfo.Capture(ex);

                    // rollback tran, etc

                    dispatchException.Throw();
                }
            }
            catch (Exception ex)
            {
                var dispatchException = ExceptionDispatchInfo.Capture(ex);

                // other rollbacks

                dispatchException.Throw();
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
            Console.WriteLine(ex.InnerException.Message);
            Console.WriteLine(ex.StackTrace);
        }

        Console.ReadLine();
    }

:输出将类似于:

Test app ex
Test inner ex
   at TestApp.Program.CallAndThrow() in D:\Projects\TestApp\TestApp\Program.cs:line 19
   at TestApp.Program.Main(String[] args) in D:\Projects\TestApp\TestApp\Program.cs:line 30
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at TestApp.Program.Main(String[] args) in D:\Projects\TestApp\TestApp\Program.cs:line 38
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at TestApp.Program.Main(String[] args) in D:\Projects\TestApp\TestApp\Program.cs:line 47

You can preserve stack trace using

ExceptionDispatchInfo.Capture(ex);

Here is code sample:

    static void CallAndThrow()
    {
        throw new ApplicationException("Test app ex", new Exception("Test inner ex"));
    }

    static void Main(string[] args)
    {
        try
        {
            try
            {
                try
                {
                    CallAndThrow();
                }
                catch (Exception ex)
                {
                    var dispatchException = ExceptionDispatchInfo.Capture(ex);

                    // rollback tran, etc

                    dispatchException.Throw();
                }
            }
            catch (Exception ex)
            {
                var dispatchException = ExceptionDispatchInfo.Capture(ex);

                // other rollbacks

                dispatchException.Throw();
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
            Console.WriteLine(ex.InnerException.Message);
            Console.WriteLine(ex.StackTrace);
        }

        Console.ReadLine();
    }

The output will be something like:

Test app ex
Test inner ex
   at TestApp.Program.CallAndThrow() in D:\Projects\TestApp\TestApp\Program.cs:line 19
   at TestApp.Program.Main(String[] args) in D:\Projects\TestApp\TestApp\Program.cs:line 30
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at TestApp.Program.Main(String[] args) in D:\Projects\TestApp\TestApp\Program.cs:line 38
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at TestApp.Program.Main(String[] args) in D:\Projects\TestApp\TestApp\Program.cs:line 47
相思碎 2024-10-19 21:41:03

好的,.NET Framework 中似乎存在一个错误,如果您抛出异常,并在同一个方法中重新抛出它,则原始行号会丢失(它将是该方法的最后一行)。

幸运的是,一个名叫 Fabrice MARGUERIE 的聪明人发现了 此错误的解决方案。下面是我的版本,您可以在 这个 .NET Fiddle 中进行测试。

private static void RethrowExceptionButPreserveStackTrace(Exception exception)
{
    System.Reflection.MethodInfo preserveStackTrace = typeof(Exception).GetMethod("InternalPreserveStackTrace",
      System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
    preserveStackTrace.Invoke(exception, null);
      throw exception;
}

现在像往常一样捕获异常,但不是抛出;只需调用此方法,瞧,原始行号将被保留!

OK, there seems to be a bug in the .NET Framework, if you throw an exception, and rethrow it in the same method, the original line number is lost (it will be the last line of the method).

Fortunatelly, a clever guy named Fabrice MARGUERIE found a solution to this bug. Below is my version, which you can test in this .NET Fiddle.

private static void RethrowExceptionButPreserveStackTrace(Exception exception)
{
    System.Reflection.MethodInfo preserveStackTrace = typeof(Exception).GetMethod("InternalPreserveStackTrace",
      System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
    preserveStackTrace.Invoke(exception, null);
      throw exception;
}

Now catch the exception as usually, but instead of throw; just call this method, and voila, the original line number will be preserved!

千鲤 2024-10-19 21:41:03

不确定这是否是设计使然,但我认为一直都是这样。

如果原始的 throw new Exception 位于单独的方法中,则 throw 的结果应包含原始方法名称和行号,然后是 main 中重新抛出异常的行号。

如果您使用 throw ex,那么结果将只是 main 中重新抛出异常的行。

换句话说, throw ex 会丢失所有堆栈跟踪,而 throw 则保留堆栈跟踪历史(即较低级别方法的详细信息)。但是,如果您的异常是通过与重新抛出相同的方法生成的,那么您可能会丢失一些信息。

注意。如果您编写一个非常简单且小型的测试程序,框架有时可以进行优化并将方法更改为内联代码,这意味着结果可能与“真实”程序不同。

Not sure whether this is by design, but I think it has always been like that.

If the original throw new Exception is in a separate method, then the result for throw should have the original method name and line number and then the line number in main where the exception is re-thrown.

If you use throw ex, then the result will just be the line in main where the exception is rethrow.

In other words, throw ex loses all the stacktrace, whereas throw preserves the stack trace history (ie details of the lower level methods). But if your exception is generated by the same method as your rethrow, then you can lose some information.

NB. If you write a very simple and small test program, the Framework can sometimes optimise things and change a method to be inline code which means the results may differ from a 'real' program.

独夜无伴 2024-10-19 21:41:03

您想要正确的线路号码吗?每个方法只需使用一次 try/catch 即可。在系统中,嗯......只是在UI层,而不是在逻辑或数据访问中,这非常烦人,因为如果你需要数据库事务,那么,它们不应该在UI层,而且你不会有正确的行号,但如果您不需要它们,无论在 catch 中是否有异常,都不要重新抛出...

5 分钟示例代码:

Menu File -> 新建项目,放置三个按钮,并在每个按钮中调用以下代码:

private void button1_Click(object sender, EventArgs e)
{
    try
    {
        Class1.testWithoutTC();
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message + Environment.NewLine + ex.StackTrace + Environment.NewLine + Environment.NewLine + "In. Ex.: " + ex.InnerException);
    }
}

private void button2_Click(object sender, EventArgs e)
{
    try
    {
        Class1.testWithTC1();
    }
    catch (Exception ex)
    {
            MessageBox.Show(ex.Message + Environment.NewLine + ex.StackTrace + Environment.NewLine + Environment.NewLine + "In. Ex.: " + ex.InnerException);
    }
}

private void button3_Click(object sender, EventArgs e)
{
    try
    {
        Class1.testWithTC2();
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message + Environment.NewLine + ex.StackTrace + Environment.NewLine + Environment.NewLine + "In. Ex.: " + ex.InnerException);
    }
}

现在,创建一个新类:

class Class1
{
    public int a;
    public static void testWithoutTC()
    {
        Class1 obj = null;
        obj.a = 1;
    }
    public static void testWithTC1()
    {
        try
        {
            Class1 obj = null;
            obj.a = 1;
        }
        catch
        {
            throw;
        }
    }
    public static void testWithTC2()
    {
        try
        {
            Class1 obj = null;
            obj.a = 1;
        }
        catch (Exception ex)
        {
            throw ex;
        }
    }
}

Run...第一个按钮很漂亮!

Do you want your right line number? Just use one try/catch per method. In systems, well... just in the UI layer, not in logic or data access, this is very annoying, because if you need database transactions, well, they shouldn't be in the UI layer, and you won't have the right line number, but if you don't need them, don't rethrow with nor without an exception in catch...

5 minutes sample code:

Menu File -> New Project, place three buttons, and call the following code in each one:

private void button1_Click(object sender, EventArgs e)
{
    try
    {
        Class1.testWithoutTC();
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message + Environment.NewLine + ex.StackTrace + Environment.NewLine + Environment.NewLine + "In. Ex.: " + ex.InnerException);
    }
}

private void button2_Click(object sender, EventArgs e)
{
    try
    {
        Class1.testWithTC1();
    }
    catch (Exception ex)
    {
            MessageBox.Show(ex.Message + Environment.NewLine + ex.StackTrace + Environment.NewLine + Environment.NewLine + "In. Ex.: " + ex.InnerException);
    }
}

private void button3_Click(object sender, EventArgs e)
{
    try
    {
        Class1.testWithTC2();
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message + Environment.NewLine + ex.StackTrace + Environment.NewLine + Environment.NewLine + "In. Ex.: " + ex.InnerException);
    }
}

Now, create a new Class:

class Class1
{
    public int a;
    public static void testWithoutTC()
    {
        Class1 obj = null;
        obj.a = 1;
    }
    public static void testWithTC1()
    {
        try
        {
            Class1 obj = null;
            obj.a = 1;
        }
        catch
        {
            throw;
        }
    }
    public static void testWithTC2()
    {
        try
        {
            Class1 obj = null;
            obj.a = 1;
        }
        catch (Exception ex)
        {
            throw ex;
        }
    }
}

Run... the first button is beautiful!

少女七分熟 2024-10-19 21:41:03

我认为这不是堆栈跟踪更改的情况,更多的是与确定堆栈跟踪的行号的方式有关。在 Visual Studio 2010 中进行尝试,其行为与您在 MSDN 文档中所期望的类似:“throw ex;”从该语句“throw;”开始重建堆栈跟踪保留堆栈跟踪,除了重新抛出异常的位置之外,行号是重新抛出的位置,而不是异常所经过的调用。

所以用“投掷;”方法调用树保持不变,但行号可能会改变。

我已经遇到过几次这种情况,这可能是设计使然,只是没有完整记录。我可以理解为什么他们可能这样做,因为知道重新抛出位置非常有用,并且如果您的方法足够简单,那么原始来源通常是显而易见的。

正如许多其他人所说,通常最好不要捕获异常,除非您确实必须这样做,和/或您要在那时处理它。

有趣的旁注:Visual Studio 2010 甚至不允许我构建问题中提供的代码,因为它在编译时出现除以零错误。

I think this is less a case of stack trace changing and more to do with the way the line number for the stack trace is determined. Trying it out in Visual Studio 2010, the behaviour is similar to what you would expect from the MSDN documentation: "throw ex;" rebuilds the stack trace from the point of this statement, "throw;" leaves the stack trace as it as, except that where ever the exception is rethrown, the line number is the location of the rethrow and not the call the exception came through.

So with "throw;" the method call tree is left unaltered, but the line numbers may change.

I've come across this a few times, and it may be by design and just not documented fully. I can understand why they may have done this as the rethrow location is very useful to know, and if your methods are simple enough the original source would usually be obvious anyway.

As many other people have said, it usually best to not catch the exception unless you really have to, and/or you are going to deal with it at that point.

Interesting side note: Visual Studio 2010 won't even let me build the code as presented in the question as it picks up the divide by zero error at compile time.

彩虹直至黑白 2024-10-19 21:41:03

这是因为您从第 12 行捕获了异常,并在第 15 行重新抛出了它,因此堆栈跟踪将其视为现金,即从那里抛出了Exception

为了更好地处理异常,您应该简单地使用try...finally,并让未处理的Exception冒泡。

That is because you catched the Exception from Line 12 and have rethrown it on Line 15, so the Stack Trace takes it as cash, that the Exception was thrown from there.

To better handle exceptions, you should simply use try...finally, and let the unhandled Exception bubble up.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文